@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.
Files changed (51) hide show
  1. package/dist/index.d.mts +2414 -0
  2. package/dist/index.d.ts +2414 -0
  3. package/dist/index.js +2788 -0
  4. package/dist/index.mjs +2678 -0
  5. package/package.json +49 -0
  6. package/src/api/client.ts +86 -0
  7. package/src/api/endpoints.ts +87 -0
  8. package/src/api/errors.ts +179 -0
  9. package/src/api/index.ts +35 -0
  10. package/src/crypto/encryption-state.ts +249 -0
  11. package/src/crypto/index.ts +55 -0
  12. package/src/crypto/types.ts +307 -0
  13. package/src/crypto/wasm-provider.ts +298 -0
  14. package/src/hooks/index.ts +31 -0
  15. package/src/hooks/keys.ts +62 -0
  16. package/src/hooks/mutations/index.ts +15 -0
  17. package/src/hooks/mutations/useDeleteMessage.ts +67 -0
  18. package/src/hooks/mutations/useEditMessage.ts +87 -0
  19. package/src/hooks/mutations/useReaction.ts +163 -0
  20. package/src/hooks/mutations/useSendMessage.ts +131 -0
  21. package/src/hooks/useChannels.ts +49 -0
  22. package/src/hooks/useMessages.ts +77 -0
  23. package/src/hooks/useSpaces.ts +60 -0
  24. package/src/index.ts +32 -0
  25. package/src/signing/index.ts +10 -0
  26. package/src/signing/types.ts +83 -0
  27. package/src/signing/wasm-provider.ts +75 -0
  28. package/src/storage/adapter.ts +118 -0
  29. package/src/storage/index.ts +9 -0
  30. package/src/sync/index.ts +83 -0
  31. package/src/sync/service.test.ts +822 -0
  32. package/src/sync/service.ts +947 -0
  33. package/src/sync/types.ts +267 -0
  34. package/src/sync/utils.ts +588 -0
  35. package/src/transport/browser-websocket.ts +299 -0
  36. package/src/transport/index.ts +34 -0
  37. package/src/transport/rn-websocket.ts +321 -0
  38. package/src/transport/types.ts +56 -0
  39. package/src/transport/websocket.ts +212 -0
  40. package/src/types/bookmark.ts +29 -0
  41. package/src/types/conversation.ts +25 -0
  42. package/src/types/index.ts +57 -0
  43. package/src/types/message.ts +178 -0
  44. package/src/types/space.ts +75 -0
  45. package/src/types/user.ts +72 -0
  46. package/src/utils/encoding.ts +106 -0
  47. package/src/utils/formatting.ts +139 -0
  48. package/src/utils/index.ts +9 -0
  49. package/src/utils/logger.ts +141 -0
  50. package/src/utils/mentions.ts +135 -0
  51. package/src/utils/validation.ts +84 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,2678 @@
1
+ // src/types/bookmark.ts
2
+ var BOOKMARKS_CONFIG = {
3
+ MAX_BOOKMARKS: 200,
4
+ PREVIEW_SNIPPET_LENGTH: 150
5
+ };
6
+
7
+ // src/api/errors.ts
8
+ var ApiErrorCode = /* @__PURE__ */ ((ApiErrorCode2) => {
9
+ ApiErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
10
+ ApiErrorCode2["TIMEOUT"] = "TIMEOUT";
11
+ ApiErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED";
12
+ ApiErrorCode2["FORBIDDEN"] = "FORBIDDEN";
13
+ ApiErrorCode2["TOKEN_EXPIRED"] = "TOKEN_EXPIRED";
14
+ ApiErrorCode2["VALIDATION_ERROR"] = "VALIDATION_ERROR";
15
+ ApiErrorCode2["BAD_REQUEST"] = "BAD_REQUEST";
16
+ ApiErrorCode2["NOT_FOUND"] = "NOT_FOUND";
17
+ ApiErrorCode2["CONFLICT"] = "CONFLICT";
18
+ ApiErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
19
+ ApiErrorCode2["SERVER_ERROR"] = "SERVER_ERROR";
20
+ ApiErrorCode2["SERVICE_UNAVAILABLE"] = "SERVICE_UNAVAILABLE";
21
+ ApiErrorCode2["UNKNOWN"] = "UNKNOWN";
22
+ return ApiErrorCode2;
23
+ })(ApiErrorCode || {});
24
+ var ApiError = class _ApiError extends Error {
25
+ constructor(details) {
26
+ super(details.message);
27
+ this.name = "ApiError";
28
+ this.code = details.code;
29
+ this.status = details.status;
30
+ this.field = details.field;
31
+ this.retryAfter = details.retryAfter;
32
+ this.originalError = details.originalError;
33
+ if (Error.captureStackTrace) {
34
+ Error.captureStackTrace(this, _ApiError);
35
+ }
36
+ }
37
+ /** Check if error is retryable */
38
+ get isRetryable() {
39
+ return [
40
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
41
+ "TIMEOUT" /* TIMEOUT */,
42
+ "RATE_LIMITED" /* RATE_LIMITED */,
43
+ "SERVER_ERROR" /* SERVER_ERROR */,
44
+ "SERVICE_UNAVAILABLE" /* SERVICE_UNAVAILABLE */
45
+ ].includes(this.code);
46
+ }
47
+ /** Check if error requires re-authentication */
48
+ get requiresAuth() {
49
+ return [
50
+ "UNAUTHORIZED" /* UNAUTHORIZED */,
51
+ "TOKEN_EXPIRED" /* TOKEN_EXPIRED */
52
+ ].includes(this.code);
53
+ }
54
+ /** Convert to JSON-serializable object */
55
+ toJSON() {
56
+ return {
57
+ code: this.code,
58
+ message: this.message,
59
+ status: this.status,
60
+ field: this.field,
61
+ retryAfter: this.retryAfter
62
+ };
63
+ }
64
+ };
65
+ function createApiError(status, message, field) {
66
+ let code;
67
+ let defaultMessage;
68
+ switch (status) {
69
+ case 400:
70
+ code = "BAD_REQUEST" /* BAD_REQUEST */;
71
+ defaultMessage = "Invalid request";
72
+ break;
73
+ case 401:
74
+ code = "UNAUTHORIZED" /* UNAUTHORIZED */;
75
+ defaultMessage = "Authentication required";
76
+ break;
77
+ case 403:
78
+ code = "FORBIDDEN" /* FORBIDDEN */;
79
+ defaultMessage = "Access denied";
80
+ break;
81
+ case 404:
82
+ code = "NOT_FOUND" /* NOT_FOUND */;
83
+ defaultMessage = "Resource not found";
84
+ break;
85
+ case 409:
86
+ code = "CONFLICT" /* CONFLICT */;
87
+ defaultMessage = "Resource conflict";
88
+ break;
89
+ case 422:
90
+ code = "VALIDATION_ERROR" /* VALIDATION_ERROR */;
91
+ defaultMessage = "Validation failed";
92
+ break;
93
+ case 429:
94
+ code = "RATE_LIMITED" /* RATE_LIMITED */;
95
+ defaultMessage = "Rate limit exceeded";
96
+ break;
97
+ case 500:
98
+ code = "SERVER_ERROR" /* SERVER_ERROR */;
99
+ defaultMessage = "Server error";
100
+ break;
101
+ case 503:
102
+ code = "SERVICE_UNAVAILABLE" /* SERVICE_UNAVAILABLE */;
103
+ defaultMessage = "Service unavailable";
104
+ break;
105
+ default:
106
+ code = "UNKNOWN" /* UNKNOWN */;
107
+ defaultMessage = "An unexpected error occurred";
108
+ }
109
+ return new ApiError({
110
+ code,
111
+ message: message || defaultMessage,
112
+ status,
113
+ field
114
+ });
115
+ }
116
+ function createNetworkError(error) {
117
+ if (error.name === "AbortError") {
118
+ return new ApiError({
119
+ code: "TIMEOUT" /* TIMEOUT */,
120
+ message: "Request timed out",
121
+ originalError: error
122
+ });
123
+ }
124
+ return new ApiError({
125
+ code: "NETWORK_ERROR" /* NETWORK_ERROR */,
126
+ message: "Network error",
127
+ originalError: error
128
+ });
129
+ }
130
+
131
+ // src/api/endpoints.ts
132
+ var endpoints = {
133
+ // Spaces
134
+ spaces: {
135
+ list: () => "/spaces",
136
+ detail: (spaceId) => `/spaces/${spaceId}`,
137
+ members: (spaceId) => `/spaces/${spaceId}/members`,
138
+ join: (spaceId) => `/spaces/${spaceId}/join`,
139
+ leave: (spaceId) => `/spaces/${spaceId}/leave`
140
+ },
141
+ // Channels
142
+ channels: {
143
+ list: (spaceId) => `/spaces/${spaceId}/channels`,
144
+ detail: (spaceId, channelId) => `/spaces/${spaceId}/channels/${channelId}`
145
+ },
146
+ // Messages
147
+ messages: {
148
+ list: (spaceId, channelId) => `/spaces/${spaceId}/channels/${channelId}/messages`,
149
+ detail: (spaceId, channelId, messageId) => `/spaces/${spaceId}/channels/${channelId}/messages/${messageId}`,
150
+ send: (spaceId, channelId) => `/spaces/${spaceId}/channels/${channelId}/messages`,
151
+ react: (spaceId, channelId, messageId) => `/spaces/${spaceId}/channels/${channelId}/messages/${messageId}/reactions`,
152
+ pin: (spaceId, channelId, messageId) => `/spaces/${spaceId}/channels/${channelId}/messages/${messageId}/pin`
153
+ },
154
+ // Conversations (DMs)
155
+ conversations: {
156
+ list: () => "/conversations",
157
+ detail: (conversationId) => `/conversations/${conversationId}`,
158
+ messages: (conversationId) => `/conversations/${conversationId}/messages`
159
+ },
160
+ // User
161
+ user: {
162
+ config: () => "/user/config",
163
+ profile: () => "/user/profile",
164
+ notifications: () => "/user/notifications"
165
+ },
166
+ // Search
167
+ search: {
168
+ messages: (spaceId) => `/spaces/${spaceId}/search/messages`,
169
+ members: (spaceId) => `/spaces/${spaceId}/search/members`
170
+ }
171
+ };
172
+
173
+ // src/hooks/keys.ts
174
+ var queryKeys = {
175
+ // Spaces
176
+ spaces: {
177
+ all: ["spaces"],
178
+ detail: (spaceId) => ["spaces", spaceId],
179
+ members: (spaceId) => ["spaces", spaceId, "members"],
180
+ member: (spaceId, address) => ["spaces", spaceId, "members", address]
181
+ },
182
+ // Channels
183
+ channels: {
184
+ bySpace: (spaceId) => ["channels", spaceId],
185
+ detail: (spaceId, channelId) => ["channels", spaceId, channelId]
186
+ },
187
+ // Messages
188
+ messages: {
189
+ infinite: (spaceId, channelId) => ["messages", "infinite", spaceId, channelId],
190
+ detail: (spaceId, channelId, messageId) => ["messages", spaceId, channelId, messageId],
191
+ pinned: (spaceId, channelId) => ["messages", "pinned", spaceId, channelId]
192
+ },
193
+ // Conversations (DMs)
194
+ conversations: {
195
+ all: (type) => ["conversations", type],
196
+ detail: (conversationId) => ["conversations", conversationId],
197
+ messages: (conversationId) => ["conversations", conversationId, "messages"]
198
+ },
199
+ // User
200
+ user: {
201
+ config: (address) => ["user", "config", address],
202
+ profile: (address) => ["user", "profile", address]
203
+ },
204
+ // Bookmarks
205
+ bookmarks: {
206
+ all: ["bookmarks"],
207
+ bySource: (sourceType) => ["bookmarks", sourceType],
208
+ bySpace: (spaceId) => ["bookmarks", "space", spaceId],
209
+ check: (messageId) => ["bookmarks", "check", messageId]
210
+ }
211
+ };
212
+
213
+ // src/hooks/useSpaces.ts
214
+ import { useQuery } from "@tanstack/react-query";
215
+ function useSpaces({ storage, enabled = true }) {
216
+ return useQuery({
217
+ queryKey: queryKeys.spaces.all,
218
+ queryFn: async () => {
219
+ return storage.getSpaces();
220
+ },
221
+ enabled,
222
+ staleTime: 1e3 * 60 * 5
223
+ // 5 minutes
224
+ });
225
+ }
226
+ function useSpace({ storage, spaceId, enabled = true }) {
227
+ return useQuery({
228
+ queryKey: queryKeys.spaces.detail(spaceId ?? ""),
229
+ queryFn: async () => {
230
+ if (!spaceId) return null;
231
+ return storage.getSpace(spaceId);
232
+ },
233
+ enabled: enabled && !!spaceId,
234
+ staleTime: 1e3 * 60 * 5
235
+ // 5 minutes
236
+ });
237
+ }
238
+ function useSpaceMembers({
239
+ storage,
240
+ spaceId,
241
+ enabled = true
242
+ }) {
243
+ return useQuery({
244
+ queryKey: queryKeys.spaces.members(spaceId ?? ""),
245
+ queryFn: async () => {
246
+ if (!spaceId) return [];
247
+ return storage.getSpaceMembers(spaceId);
248
+ },
249
+ enabled: enabled && !!spaceId,
250
+ staleTime: 1e3 * 60 * 2
251
+ // 2 minutes
252
+ });
253
+ }
254
+
255
+ // src/hooks/useChannels.ts
256
+ import { useQuery as useQuery2 } from "@tanstack/react-query";
257
+ function useChannels({ storage, spaceId, enabled = true }) {
258
+ return useQuery2({
259
+ queryKey: queryKeys.channels.bySpace(spaceId ?? ""),
260
+ queryFn: async () => {
261
+ if (!spaceId) return [];
262
+ return storage.getChannels(spaceId);
263
+ },
264
+ enabled: enabled && !!spaceId,
265
+ staleTime: 1e3 * 60 * 5
266
+ // 5 minutes
267
+ });
268
+ }
269
+ function flattenChannels(groups) {
270
+ return groups.flatMap((group) => group.channels);
271
+ }
272
+ function findChannel(groups, channelId) {
273
+ for (const group of groups) {
274
+ const channel = group.channels.find((c) => c.channelId === channelId);
275
+ if (channel) return channel;
276
+ }
277
+ return void 0;
278
+ }
279
+
280
+ // src/hooks/useMessages.ts
281
+ import { useInfiniteQuery, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
282
+ function useMessages({
283
+ storage,
284
+ spaceId,
285
+ channelId,
286
+ enabled = true,
287
+ limit = 50
288
+ }) {
289
+ return useInfiniteQuery({
290
+ queryKey: queryKeys.messages.infinite(spaceId ?? "", channelId ?? ""),
291
+ queryFn: async ({ pageParam }) => {
292
+ if (!spaceId || !channelId) {
293
+ return { messages: [], nextCursor: null, prevCursor: null };
294
+ }
295
+ return storage.getMessages({
296
+ spaceId,
297
+ channelId,
298
+ cursor: pageParam,
299
+ direction: "backward",
300
+ limit
301
+ });
302
+ },
303
+ getNextPageParam: (lastPage) => lastPage.prevCursor,
304
+ getPreviousPageParam: (firstPage) => firstPage.nextCursor,
305
+ initialPageParam: void 0,
306
+ enabled: enabled && !!spaceId && !!channelId,
307
+ staleTime: 1e3 * 30
308
+ // 30 seconds
309
+ });
310
+ }
311
+ function flattenMessages(pages) {
312
+ if (!pages) return [];
313
+ return pages.flatMap((page) => page.messages);
314
+ }
315
+ function useInvalidateMessages() {
316
+ const queryClient = useQueryClient2();
317
+ return {
318
+ invalidateChannel: (spaceId, channelId) => {
319
+ queryClient.invalidateQueries({
320
+ queryKey: queryKeys.messages.infinite(spaceId, channelId)
321
+ });
322
+ },
323
+ invalidateSpace: (spaceId) => {
324
+ queryClient.invalidateQueries({
325
+ queryKey: ["messages", "infinite", spaceId]
326
+ });
327
+ }
328
+ };
329
+ }
330
+
331
+ // src/hooks/mutations/useSendMessage.ts
332
+ import { useMutation, useQueryClient as useQueryClient3 } from "@tanstack/react-query";
333
+ function useSendMessage({
334
+ storage,
335
+ apiClient,
336
+ currentUserId
337
+ }) {
338
+ const queryClient = useQueryClient3();
339
+ return useMutation({
340
+ mutationFn: async (params) => {
341
+ return apiClient.sendMessage(params);
342
+ },
343
+ onMutate: async (params) => {
344
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
345
+ await queryClient.cancelQueries({ queryKey: key });
346
+ const previousData = queryClient.getQueryData(key);
347
+ const optimisticMessage = {
348
+ messageId: `temp-${Date.now()}-${Math.random().toString(36).slice(2)}`,
349
+ channelId: params.channelId,
350
+ spaceId: params.spaceId,
351
+ digestAlgorithm: "",
352
+ nonce: "",
353
+ createdDate: Date.now(),
354
+ modifiedDate: Date.now(),
355
+ lastModifiedHash: "",
356
+ content: {
357
+ type: "post",
358
+ senderId: currentUserId,
359
+ text: params.text,
360
+ repliesToMessageId: params.repliesToMessageId
361
+ },
362
+ reactions: [],
363
+ mentions: { memberIds: [], roleIds: [], channelIds: [] },
364
+ sendStatus: "sending"
365
+ };
366
+ queryClient.setQueryData(key, (old) => {
367
+ if (!old) return old;
368
+ return {
369
+ ...old,
370
+ pages: old.pages.map((page, index) => {
371
+ if (index === 0) {
372
+ return {
373
+ ...page,
374
+ messages: [optimisticMessage, ...page.messages]
375
+ };
376
+ }
377
+ return page;
378
+ })
379
+ };
380
+ });
381
+ return { previousData, optimisticMessage };
382
+ },
383
+ onError: (err, params, context) => {
384
+ if (context?.previousData) {
385
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
386
+ queryClient.setQueryData(key, context.previousData);
387
+ }
388
+ },
389
+ onSuccess: async (message, params, context) => {
390
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
391
+ queryClient.setQueryData(key, (old) => {
392
+ if (!old) return old;
393
+ return {
394
+ ...old,
395
+ pages: old.pages.map((page, index) => {
396
+ if (index === 0) {
397
+ return {
398
+ ...page,
399
+ messages: page.messages.map(
400
+ (m) => m.messageId === context?.optimisticMessage.messageId ? message : m
401
+ )
402
+ };
403
+ }
404
+ return page;
405
+ })
406
+ };
407
+ });
408
+ await storage.saveMessage(
409
+ message,
410
+ message.createdDate,
411
+ currentUserId,
412
+ "space",
413
+ "",
414
+ ""
415
+ );
416
+ },
417
+ onSettled: (data, err, params) => {
418
+ queryClient.invalidateQueries({
419
+ queryKey: queryKeys.messages.infinite(params.spaceId, params.channelId)
420
+ });
421
+ }
422
+ });
423
+ }
424
+
425
+ // src/hooks/mutations/useReaction.ts
426
+ import { useMutation as useMutation2, useQueryClient as useQueryClient4 } from "@tanstack/react-query";
427
+ function useAddReaction({
428
+ storage,
429
+ apiClient,
430
+ currentUserId
431
+ }) {
432
+ const queryClient = useQueryClient4();
433
+ return useMutation2({
434
+ mutationFn: async (params) => {
435
+ return apiClient.addReaction(params);
436
+ },
437
+ onMutate: async (params) => {
438
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
439
+ await queryClient.cancelQueries({ queryKey: key });
440
+ const previousData = queryClient.getQueryData(key);
441
+ queryClient.setQueryData(key, (old) => {
442
+ if (!old) return old;
443
+ return {
444
+ ...old,
445
+ pages: old.pages.map((page) => ({
446
+ ...page,
447
+ messages: page.messages.map((message) => {
448
+ if (message.messageId !== params.messageId) return message;
449
+ const existingReaction = message.reactions.find(
450
+ (r) => r.emojiName === params.reaction
451
+ );
452
+ if (existingReaction) {
453
+ return {
454
+ ...message,
455
+ reactions: message.reactions.map(
456
+ (r) => r.emojiName === params.reaction ? {
457
+ ...r,
458
+ count: r.count + 1,
459
+ memberIds: [...r.memberIds, currentUserId]
460
+ } : r
461
+ )
462
+ };
463
+ } else {
464
+ const newReaction = {
465
+ emojiId: params.reaction,
466
+ emojiName: params.reaction,
467
+ spaceId: params.spaceId,
468
+ count: 1,
469
+ memberIds: [currentUserId]
470
+ };
471
+ return {
472
+ ...message,
473
+ reactions: [...message.reactions, newReaction]
474
+ };
475
+ }
476
+ })
477
+ }))
478
+ };
479
+ });
480
+ return { previousData };
481
+ },
482
+ onError: (err, params, context) => {
483
+ if (context?.previousData) {
484
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
485
+ queryClient.setQueryData(key, context.previousData);
486
+ }
487
+ }
488
+ });
489
+ }
490
+ function useRemoveReaction({
491
+ storage,
492
+ apiClient,
493
+ currentUserId
494
+ }) {
495
+ const queryClient = useQueryClient4();
496
+ return useMutation2({
497
+ mutationFn: async (params) => {
498
+ return apiClient.removeReaction(params);
499
+ },
500
+ onMutate: async (params) => {
501
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
502
+ await queryClient.cancelQueries({ queryKey: key });
503
+ const previousData = queryClient.getQueryData(key);
504
+ queryClient.setQueryData(key, (old) => {
505
+ if (!old) return old;
506
+ return {
507
+ ...old,
508
+ pages: old.pages.map((page) => ({
509
+ ...page,
510
+ messages: page.messages.map((message) => {
511
+ if (message.messageId !== params.messageId) return message;
512
+ return {
513
+ ...message,
514
+ reactions: message.reactions.map((r) => {
515
+ if (r.emojiName !== params.reaction) return r;
516
+ const newMemberIds = r.memberIds.filter(
517
+ (id) => id !== currentUserId
518
+ );
519
+ if (newMemberIds.length === 0) {
520
+ return null;
521
+ }
522
+ return {
523
+ ...r,
524
+ count: newMemberIds.length,
525
+ memberIds: newMemberIds
526
+ };
527
+ }).filter((r) => r !== null)
528
+ };
529
+ })
530
+ }))
531
+ };
532
+ });
533
+ return { previousData };
534
+ },
535
+ onError: (err, params, context) => {
536
+ if (context?.previousData) {
537
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
538
+ queryClient.setQueryData(key, context.previousData);
539
+ }
540
+ }
541
+ });
542
+ }
543
+
544
+ // src/hooks/mutations/useEditMessage.ts
545
+ import { useMutation as useMutation3, useQueryClient as useQueryClient5 } from "@tanstack/react-query";
546
+ function useEditMessage({ storage, apiClient }) {
547
+ const queryClient = useQueryClient5();
548
+ return useMutation3({
549
+ mutationFn: async (params) => {
550
+ return apiClient.editMessage(params);
551
+ },
552
+ onMutate: async (params) => {
553
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
554
+ await queryClient.cancelQueries({ queryKey: key });
555
+ const previousData = queryClient.getQueryData(key);
556
+ queryClient.setQueryData(key, (old) => {
557
+ if (!old) return old;
558
+ return {
559
+ ...old,
560
+ pages: old.pages.map((page) => ({
561
+ ...page,
562
+ messages: page.messages.map((message) => {
563
+ if (message.messageId !== params.messageId) return message;
564
+ if (message.content.type !== "post") return message;
565
+ return {
566
+ ...message,
567
+ modifiedDate: Date.now(),
568
+ content: {
569
+ ...message.content,
570
+ text: params.text
571
+ }
572
+ };
573
+ })
574
+ }))
575
+ };
576
+ });
577
+ return { previousData };
578
+ },
579
+ onError: (err, params, context) => {
580
+ if (context?.previousData) {
581
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
582
+ queryClient.setQueryData(key, context.previousData);
583
+ }
584
+ },
585
+ onSuccess: async (message) => {
586
+ await storage.saveMessage(
587
+ message,
588
+ message.modifiedDate,
589
+ message.content.senderId,
590
+ "space",
591
+ "",
592
+ ""
593
+ );
594
+ },
595
+ onSettled: (data, err, params) => {
596
+ queryClient.invalidateQueries({
597
+ queryKey: queryKeys.messages.infinite(params.spaceId, params.channelId)
598
+ });
599
+ }
600
+ });
601
+ }
602
+
603
+ // src/hooks/mutations/useDeleteMessage.ts
604
+ import { useMutation as useMutation4, useQueryClient as useQueryClient6 } from "@tanstack/react-query";
605
+ function useDeleteMessage({ storage, apiClient }) {
606
+ const queryClient = useQueryClient6();
607
+ return useMutation4({
608
+ mutationFn: async (params) => {
609
+ return apiClient.deleteMessage(params);
610
+ },
611
+ onMutate: async (params) => {
612
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
613
+ await queryClient.cancelQueries({ queryKey: key });
614
+ const previousData = queryClient.getQueryData(key);
615
+ queryClient.setQueryData(key, (old) => {
616
+ if (!old) return old;
617
+ return {
618
+ ...old,
619
+ pages: old.pages.map((page) => ({
620
+ ...page,
621
+ messages: page.messages.filter(
622
+ (message) => message.messageId !== params.messageId
623
+ )
624
+ }))
625
+ };
626
+ });
627
+ return { previousData };
628
+ },
629
+ onError: (err, params, context) => {
630
+ if (context?.previousData) {
631
+ const key = queryKeys.messages.infinite(params.spaceId, params.channelId);
632
+ queryClient.setQueryData(key, context.previousData);
633
+ }
634
+ },
635
+ onSuccess: async (_, params) => {
636
+ await storage.deleteMessage(params.messageId);
637
+ },
638
+ onSettled: (data, err, params) => {
639
+ queryClient.invalidateQueries({
640
+ queryKey: queryKeys.messages.infinite(params.spaceId, params.channelId)
641
+ });
642
+ }
643
+ });
644
+ }
645
+
646
+ // src/utils/validation.ts
647
+ var MAX_MESSAGE_LENGTH = 4e3;
648
+ var MAX_MENTIONS = 50;
649
+ function validateMessageContent(content) {
650
+ const errors = [];
651
+ if (!content || content.trim().length === 0) {
652
+ errors.push("Message cannot be empty");
653
+ }
654
+ if (content.length > MAX_MESSAGE_LENGTH) {
655
+ errors.push(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
656
+ }
657
+ return {
658
+ valid: errors.length === 0,
659
+ errors
660
+ };
661
+ }
662
+ function validateMessage(message) {
663
+ const errors = [];
664
+ if (!message.messageId) {
665
+ errors.push("Message ID is required");
666
+ }
667
+ if (!message.channelId) {
668
+ errors.push("Channel ID is required");
669
+ }
670
+ if (!message.spaceId) {
671
+ errors.push("Space ID is required");
672
+ }
673
+ if (!message.content) {
674
+ errors.push("Message content is required");
675
+ }
676
+ if (message.content?.type === "post") {
677
+ const postContent = message.content;
678
+ const text = Array.isArray(postContent.text) ? postContent.text.join("") : postContent.text;
679
+ if (text.length > MAX_MESSAGE_LENGTH) {
680
+ errors.push(`Message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
681
+ }
682
+ }
683
+ return {
684
+ valid: errors.length === 0,
685
+ errors
686
+ };
687
+ }
688
+ function sanitizeContent(content) {
689
+ return content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
690
+ }
691
+
692
+ // src/utils/mentions.ts
693
+ var MENTION_PATTERNS = {
694
+ user: /<@([a-zA-Z0-9]+)>/g,
695
+ role: /<@&([a-zA-Z0-9]+)>/g,
696
+ channel: /<#([a-zA-Z0-9]+)>/g,
697
+ everyone: /@everyone/g,
698
+ here: /@here/g
699
+ };
700
+ function parseMentions(text) {
701
+ const mentions = [];
702
+ let match;
703
+ const userRegex = new RegExp(MENTION_PATTERNS.user.source, "g");
704
+ while ((match = userRegex.exec(text)) !== null) {
705
+ mentions.push({
706
+ type: "user",
707
+ id: match[1],
708
+ raw: match[0],
709
+ start: match.index,
710
+ end: match.index + match[0].length
711
+ });
712
+ }
713
+ const roleRegex = new RegExp(MENTION_PATTERNS.role.source, "g");
714
+ while ((match = roleRegex.exec(text)) !== null) {
715
+ mentions.push({
716
+ type: "role",
717
+ id: match[1],
718
+ raw: match[0],
719
+ start: match.index,
720
+ end: match.index + match[0].length
721
+ });
722
+ }
723
+ const channelRegex = new RegExp(MENTION_PATTERNS.channel.source, "g");
724
+ while ((match = channelRegex.exec(text)) !== null) {
725
+ mentions.push({
726
+ type: "channel",
727
+ id: match[1],
728
+ raw: match[0],
729
+ start: match.index,
730
+ end: match.index + match[0].length
731
+ });
732
+ }
733
+ const everyoneRegex = new RegExp(MENTION_PATTERNS.everyone.source, "g");
734
+ while ((match = everyoneRegex.exec(text)) !== null) {
735
+ mentions.push({
736
+ type: "everyone",
737
+ raw: match[0],
738
+ start: match.index,
739
+ end: match.index + match[0].length
740
+ });
741
+ }
742
+ const hereRegex = new RegExp(MENTION_PATTERNS.here.source, "g");
743
+ while ((match = hereRegex.exec(text)) !== null) {
744
+ mentions.push({
745
+ type: "here",
746
+ raw: match[0],
747
+ start: match.index,
748
+ end: match.index + match[0].length
749
+ });
750
+ }
751
+ return mentions.sort((a, b) => a.start - b.start);
752
+ }
753
+ function extractMentions(text) {
754
+ const parsed = parseMentions(text);
755
+ const memberIds = parsed.filter((m) => m.type === "user" && m.id).map((m) => m.id);
756
+ const roleIds = parsed.filter((m) => m.type === "role" && m.id).map((m) => m.id);
757
+ const channelIds = parsed.filter((m) => m.type === "channel" && m.id).map((m) => m.id);
758
+ const everyone = parsed.some((m) => m.type === "everyone" || m.type === "here");
759
+ return {
760
+ memberIds: [...new Set(memberIds)],
761
+ roleIds: [...new Set(roleIds)],
762
+ channelIds: [...new Set(channelIds)],
763
+ everyone,
764
+ totalMentionCount: memberIds.length + roleIds.length + channelIds.length
765
+ };
766
+ }
767
+ function formatMention(type, id) {
768
+ switch (type) {
769
+ case "user":
770
+ return `<@${id}>`;
771
+ case "role":
772
+ return `<@&${id}>`;
773
+ case "channel":
774
+ return `<#${id}>`;
775
+ }
776
+ }
777
+
778
+ // src/utils/formatting.ts
779
+ function formatTime(timestamp) {
780
+ const date = new Date(timestamp);
781
+ return date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
782
+ }
783
+ function formatDate(timestamp) {
784
+ const date = new Date(timestamp);
785
+ return date.toLocaleDateString([], {
786
+ month: "short",
787
+ day: "numeric",
788
+ year: "numeric"
789
+ });
790
+ }
791
+ function formatDateTime(timestamp) {
792
+ const date = new Date(timestamp);
793
+ return date.toLocaleString([], {
794
+ month: "short",
795
+ day: "numeric",
796
+ year: "numeric",
797
+ hour: "numeric",
798
+ minute: "2-digit"
799
+ });
800
+ }
801
+ function formatRelativeTime(timestamp) {
802
+ const now = Date.now();
803
+ const diff = now - timestamp;
804
+ const seconds = Math.floor(diff / 1e3);
805
+ const minutes = Math.floor(seconds / 60);
806
+ const hours = Math.floor(minutes / 60);
807
+ const days = Math.floor(hours / 24);
808
+ if (seconds < 60) {
809
+ return "Just now";
810
+ }
811
+ if (minutes < 60) {
812
+ return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
813
+ }
814
+ if (hours < 24) {
815
+ return `${hours} hour${hours === 1 ? "" : "s"} ago`;
816
+ }
817
+ if (days === 1) {
818
+ return "Yesterday";
819
+ }
820
+ if (days < 7) {
821
+ return `${days} days ago`;
822
+ }
823
+ return formatDate(timestamp);
824
+ }
825
+ function formatMessageDate(timestamp) {
826
+ const date = new Date(timestamp);
827
+ const today = /* @__PURE__ */ new Date();
828
+ const yesterday = new Date(today);
829
+ yesterday.setDate(yesterday.getDate() - 1);
830
+ if (isSameDay(date, today)) {
831
+ return "Today";
832
+ }
833
+ if (isSameDay(date, yesterday)) {
834
+ return "Yesterday";
835
+ }
836
+ return date.toLocaleDateString([], {
837
+ weekday: "long",
838
+ month: "long",
839
+ day: "numeric",
840
+ year: date.getFullYear() !== today.getFullYear() ? "numeric" : void 0
841
+ });
842
+ }
843
+ function isSameDay(d1, d2) {
844
+ return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
845
+ }
846
+ function truncateText(text, maxLength) {
847
+ if (text.length <= maxLength) {
848
+ return text;
849
+ }
850
+ return text.slice(0, maxLength - 1) + "\u2026";
851
+ }
852
+ function formatFileSize(bytes) {
853
+ if (bytes === 0) return "0 B";
854
+ const k = 1024;
855
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
856
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
857
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
858
+ }
859
+ function formatMemberCount(count) {
860
+ if (count < 1e3) {
861
+ return `${count} member${count === 1 ? "" : "s"}`;
862
+ }
863
+ if (count < 1e6) {
864
+ return `${(count / 1e3).toFixed(1)}K members`;
865
+ }
866
+ return `${(count / 1e6).toFixed(1)}M members`;
867
+ }
868
+
869
+ // src/utils/encoding.ts
870
+ function hexToBytes(hex) {
871
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
872
+ const bytes = [];
873
+ for (let i = 0; i < cleanHex.length; i += 2) {
874
+ bytes.push(parseInt(cleanHex.substr(i, 2), 16));
875
+ }
876
+ return bytes;
877
+ }
878
+ function bytesToHex(bytes) {
879
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
880
+ }
881
+ function base64ToBytes(base64) {
882
+ if (typeof atob === "function") {
883
+ const binaryString = atob(base64);
884
+ const bytes = new Uint8Array(binaryString.length);
885
+ for (let i = 0; i < binaryString.length; i++) {
886
+ bytes[i] = binaryString.charCodeAt(i);
887
+ }
888
+ return bytes;
889
+ } else {
890
+ return new Uint8Array(Buffer.from(base64, "base64"));
891
+ }
892
+ }
893
+ function bytesToBase64(bytes) {
894
+ if (typeof btoa === "function") {
895
+ const binaryString = Array.from(bytes).map((b) => String.fromCharCode(b)).join("");
896
+ return btoa(binaryString);
897
+ } else {
898
+ return Buffer.from(bytes).toString("base64");
899
+ }
900
+ }
901
+ function stringToBytes(str) {
902
+ const encoder = new TextEncoder();
903
+ return Array.from(encoder.encode(str));
904
+ }
905
+ function bytesToString(bytes) {
906
+ const decoder = new TextDecoder();
907
+ return decoder.decode(new Uint8Array(bytes));
908
+ }
909
+ function int64ToBytes(num) {
910
+ const arr = new Uint8Array(8);
911
+ const view = new DataView(arr.buffer);
912
+ view.setBigInt64(0, BigInt(num), false);
913
+ return arr;
914
+ }
915
+
916
+ // src/utils/logger.ts
917
+ var LOG_LEVELS = {
918
+ debug: 0,
919
+ log: 1,
920
+ info: 2,
921
+ warn: 3,
922
+ error: 4
923
+ };
924
+ var config = {
925
+ enabled: true,
926
+ // Will be set based on environment
927
+ minLevel: "log"
928
+ };
929
+ function detectEnvironment() {
930
+ if (typeof __DEV__ !== "undefined") {
931
+ return __DEV__;
932
+ }
933
+ if (typeof process !== "undefined" && process.env) {
934
+ return process.env.NODE_ENV !== "production";
935
+ }
936
+ if (typeof window !== "undefined") {
937
+ return window.location?.hostname === "localhost";
938
+ }
939
+ return true;
940
+ }
941
+ config.enabled = detectEnvironment();
942
+ function shouldLog(level) {
943
+ if (!config.enabled) return false;
944
+ return LOG_LEVELS[level] >= LOG_LEVELS[config.minLevel];
945
+ }
946
+ function createLogMethod(level) {
947
+ return (...args) => {
948
+ if (shouldLog(level)) {
949
+ console[level](...args);
950
+ }
951
+ };
952
+ }
953
+ var logger = {
954
+ /**
955
+ * Configure the logger
956
+ */
957
+ configure(newConfig) {
958
+ config = { ...config, ...newConfig };
959
+ },
960
+ /**
961
+ * Check if logging is enabled
962
+ */
963
+ isEnabled() {
964
+ return config.enabled;
965
+ },
966
+ /**
967
+ * Enable logging (useful for debugging production issues)
968
+ */
969
+ enable() {
970
+ config.enabled = true;
971
+ },
972
+ /**
973
+ * Disable logging
974
+ */
975
+ disable() {
976
+ config.enabled = false;
977
+ },
978
+ /**
979
+ * Log at debug level
980
+ */
981
+ debug: createLogMethod("debug"),
982
+ /**
983
+ * Log at default level
984
+ */
985
+ log: createLogMethod("log"),
986
+ /**
987
+ * Log at info level
988
+ */
989
+ info: createLogMethod("info"),
990
+ /**
991
+ * Log at warn level
992
+ */
993
+ warn: createLogMethod("warn"),
994
+ /**
995
+ * Log at error level (always logs unless explicitly disabled)
996
+ */
997
+ error: createLogMethod("error"),
998
+ /**
999
+ * Create a scoped logger with a prefix
1000
+ */
1001
+ scope(prefix) {
1002
+ return {
1003
+ debug: (...args) => logger.debug(prefix, ...args),
1004
+ log: (...args) => logger.log(prefix, ...args),
1005
+ info: (...args) => logger.info(prefix, ...args),
1006
+ warn: (...args) => logger.warn(prefix, ...args),
1007
+ error: (...args) => logger.error(prefix, ...args)
1008
+ };
1009
+ }
1010
+ };
1011
+
1012
+ // src/crypto/encryption-state.ts
1013
+ var ENCRYPTION_STORAGE_KEYS = {
1014
+ /** enc_state:{conversationId}:{inboxId} */
1015
+ ENCRYPTION_STATE: "enc_state:",
1016
+ /** inbox_map:{inboxId} */
1017
+ INBOX_MAPPING: "inbox_map:",
1018
+ /** latest:{conversationId} */
1019
+ LATEST_STATE: "latest:",
1020
+ /** conv_inboxes:{conversationId} -> inboxId[] */
1021
+ CONVERSATION_INBOXES: "conv_inboxes:",
1022
+ /** conv_inbox_key:{conversationId} -> ConversationInboxKeypair */
1023
+ CONVERSATION_INBOX_KEY: "conv_inbox_key:"
1024
+ };
1025
+
1026
+ // src/crypto/wasm-provider.ts
1027
+ function parseWasmResult(result) {
1028
+ if (result.startsWith("invalid") || result.startsWith("error") || result.includes("failed") || result.includes("Error")) {
1029
+ throw new Error(result);
1030
+ }
1031
+ try {
1032
+ return JSON.parse(result);
1033
+ } catch {
1034
+ if (result.startsWith('"') && result.endsWith('"')) {
1035
+ return result.slice(1, -1);
1036
+ }
1037
+ throw new Error(`Failed to parse WASM result: ${result}`);
1038
+ }
1039
+ }
1040
+ var WasmCryptoProvider = class {
1041
+ constructor(wasmModule) {
1042
+ this.wasm = wasmModule;
1043
+ }
1044
+ // ============ Key Generation ============
1045
+ async generateX448() {
1046
+ const result = this.wasm.js_generate_x448();
1047
+ const keypair = parseWasmResult(result);
1048
+ return {
1049
+ type: "x448",
1050
+ public_key: keypair.public_key,
1051
+ private_key: keypair.private_key
1052
+ };
1053
+ }
1054
+ async generateEd448() {
1055
+ const result = this.wasm.js_generate_ed448();
1056
+ const keypair = parseWasmResult(result);
1057
+ return {
1058
+ type: "ed448",
1059
+ public_key: keypair.public_key,
1060
+ private_key: keypair.private_key
1061
+ };
1062
+ }
1063
+ async getPublicKeyX448(privateKey) {
1064
+ const result = this.wasm.js_get_pubkey_x448(privateKey);
1065
+ return parseWasmResult(result);
1066
+ }
1067
+ async getPublicKeyEd448(privateKey) {
1068
+ const result = this.wasm.js_get_pubkey_ed448(privateKey);
1069
+ return parseWasmResult(result);
1070
+ }
1071
+ // ============ X3DH Key Agreement ============
1072
+ async senderX3DH(params) {
1073
+ const input = JSON.stringify({
1074
+ sending_identity_private_key: params.sending_identity_private_key,
1075
+ sending_ephemeral_private_key: params.sending_ephemeral_private_key,
1076
+ receiving_identity_key: params.receiving_identity_key,
1077
+ receiving_signed_pre_key: params.receiving_signed_pre_key,
1078
+ session_key_length: params.session_key_length
1079
+ });
1080
+ const result = this.wasm.js_sender_x3dh(input);
1081
+ if (result.startsWith("invalid") || result.includes("error")) {
1082
+ throw new Error(result);
1083
+ }
1084
+ return result;
1085
+ }
1086
+ async receiverX3DH(params) {
1087
+ const input = JSON.stringify({
1088
+ sending_identity_private_key: params.sending_identity_private_key,
1089
+ sending_signed_private_key: params.sending_signed_private_key,
1090
+ receiving_identity_key: params.receiving_identity_key,
1091
+ receiving_ephemeral_key: params.receiving_ephemeral_key,
1092
+ session_key_length: params.session_key_length
1093
+ });
1094
+ const result = this.wasm.js_receiver_x3dh(input);
1095
+ if (result.startsWith("invalid") || result.includes("error")) {
1096
+ throw new Error(result);
1097
+ }
1098
+ return result;
1099
+ }
1100
+ // ============ Double Ratchet ============
1101
+ async newDoubleRatchet(params) {
1102
+ const input = JSON.stringify({
1103
+ session_key: params.session_key,
1104
+ sending_header_key: params.sending_header_key,
1105
+ next_receiving_header_key: params.next_receiving_header_key,
1106
+ is_sender: params.is_sender,
1107
+ sending_ephemeral_private_key: params.sending_ephemeral_private_key,
1108
+ receiving_ephemeral_key: params.receiving_ephemeral_key
1109
+ });
1110
+ const result = this.wasm.js_new_double_ratchet(input);
1111
+ if (result.startsWith("invalid") || result.includes("error")) {
1112
+ throw new Error(result);
1113
+ }
1114
+ return result;
1115
+ }
1116
+ async doubleRatchetEncrypt(stateAndMessage) {
1117
+ const input = JSON.stringify({
1118
+ ratchet_state: stateAndMessage.ratchet_state,
1119
+ message: stateAndMessage.message
1120
+ });
1121
+ const result = this.wasm.js_double_ratchet_encrypt(input);
1122
+ return parseWasmResult(result);
1123
+ }
1124
+ async doubleRatchetDecrypt(stateAndEnvelope) {
1125
+ const input = JSON.stringify({
1126
+ ratchet_state: stateAndEnvelope.ratchet_state,
1127
+ envelope: stateAndEnvelope.envelope
1128
+ });
1129
+ const result = this.wasm.js_double_ratchet_decrypt(input);
1130
+ return parseWasmResult(result);
1131
+ }
1132
+ // ============ Triple Ratchet ============
1133
+ async newTripleRatchet(params) {
1134
+ const input = JSON.stringify({
1135
+ peers: params.peers,
1136
+ peer_key: params.peer_key,
1137
+ identity_key: params.identity_key,
1138
+ signed_pre_key: params.signed_pre_key,
1139
+ threshold: params.threshold,
1140
+ async_dkg_ratchet: params.async_dkg_ratchet
1141
+ });
1142
+ const result = this.wasm.js_new_triple_ratchet(input);
1143
+ return parseWasmResult(result);
1144
+ }
1145
+ async tripleRatchetInitRound1(state) {
1146
+ const input = JSON.stringify(state);
1147
+ const result = this.wasm.js_triple_ratchet_init_round_1(input);
1148
+ return parseWasmResult(result);
1149
+ }
1150
+ async tripleRatchetInitRound2(state) {
1151
+ const input = JSON.stringify(state);
1152
+ const result = this.wasm.js_triple_ratchet_init_round_2(input);
1153
+ return parseWasmResult(result);
1154
+ }
1155
+ async tripleRatchetInitRound3(state) {
1156
+ const input = JSON.stringify(state);
1157
+ const result = this.wasm.js_triple_ratchet_init_round_3(input);
1158
+ return parseWasmResult(result);
1159
+ }
1160
+ async tripleRatchetInitRound4(state) {
1161
+ const input = JSON.stringify(state);
1162
+ const result = this.wasm.js_triple_ratchet_init_round_4(input);
1163
+ return parseWasmResult(result);
1164
+ }
1165
+ async tripleRatchetEncrypt(stateAndMessage) {
1166
+ const input = JSON.stringify({
1167
+ ratchet_state: stateAndMessage.ratchet_state,
1168
+ message: stateAndMessage.message
1169
+ });
1170
+ const result = this.wasm.js_triple_ratchet_encrypt(input);
1171
+ return parseWasmResult(result);
1172
+ }
1173
+ async tripleRatchetDecrypt(stateAndEnvelope) {
1174
+ const input = JSON.stringify({
1175
+ ratchet_state: stateAndEnvelope.ratchet_state,
1176
+ envelope: stateAndEnvelope.envelope
1177
+ });
1178
+ const result = this.wasm.js_triple_ratchet_decrypt(input);
1179
+ return parseWasmResult(result);
1180
+ }
1181
+ async tripleRatchetResize(state) {
1182
+ const input = JSON.stringify(state);
1183
+ const result = this.wasm.js_triple_ratchet_resize(input);
1184
+ return parseWasmResult(result);
1185
+ }
1186
+ // ============ Inbox Message Encryption ============
1187
+ async encryptInboxMessage(request) {
1188
+ const input = JSON.stringify({
1189
+ inbox_public_key: request.inbox_public_key,
1190
+ ephemeral_private_key: request.ephemeral_private_key,
1191
+ plaintext: request.plaintext
1192
+ });
1193
+ const result = this.wasm.js_encrypt_inbox_message(input);
1194
+ if (result.startsWith("invalid") || result.includes("error")) {
1195
+ throw new Error(result);
1196
+ }
1197
+ return result;
1198
+ }
1199
+ async decryptInboxMessage(request) {
1200
+ const input = JSON.stringify({
1201
+ inbox_private_key: request.inbox_private_key,
1202
+ ephemeral_public_key: request.ephemeral_public_key,
1203
+ ciphertext: request.ciphertext
1204
+ });
1205
+ const result = this.wasm.js_decrypt_inbox_message(input);
1206
+ return parseWasmResult(result);
1207
+ }
1208
+ };
1209
+
1210
+ // src/signing/types.ts
1211
+ async function verifySignedMessage(provider, message) {
1212
+ const payload = `${message.content}:${message.timestamp}`;
1213
+ const payloadBase64 = btoa(payload);
1214
+ return provider.verifyEd448(message.publicKey, payloadBase64, message.signature);
1215
+ }
1216
+ async function createSignedMessage(provider, privateKey, publicKey, content) {
1217
+ const timestamp = Date.now();
1218
+ const payload = `${content}:${timestamp}`;
1219
+ const payloadBase64 = btoa(payload);
1220
+ const signature = await provider.signEd448(privateKey, payloadBase64);
1221
+ return {
1222
+ content,
1223
+ signature,
1224
+ publicKey,
1225
+ timestamp
1226
+ };
1227
+ }
1228
+
1229
+ // src/signing/wasm-provider.ts
1230
+ function parseVerifyResult(result) {
1231
+ const normalized = result.toLowerCase().trim();
1232
+ if (normalized === "true" || normalized === "valid") {
1233
+ return true;
1234
+ }
1235
+ if (normalized === "false" || normalized === "invalid") {
1236
+ return false;
1237
+ }
1238
+ if (result.startsWith("invalid") || result.startsWith("error") || result.includes("failed") || result.includes("Error")) {
1239
+ throw new Error(result);
1240
+ }
1241
+ try {
1242
+ return JSON.parse(result);
1243
+ } catch {
1244
+ throw new Error(`Unexpected verification result: ${result}`);
1245
+ }
1246
+ }
1247
+ var WasmSigningProvider = class {
1248
+ constructor(wasmModule) {
1249
+ this.wasm = wasmModule;
1250
+ }
1251
+ async signEd448(privateKey, message) {
1252
+ const result = this.wasm.js_sign_ed448(privateKey, message);
1253
+ if (result.startsWith("invalid") || result.startsWith("error") || result.includes("failed") || result.includes("Error")) {
1254
+ throw new Error(result);
1255
+ }
1256
+ if (result.startsWith('"') && result.endsWith('"')) {
1257
+ return result.slice(1, -1);
1258
+ }
1259
+ return result;
1260
+ }
1261
+ async verifyEd448(publicKey, message, signature) {
1262
+ const result = this.wasm.js_verify_ed448(publicKey, message, signature);
1263
+ return parseVerifyResult(result);
1264
+ }
1265
+ };
1266
+
1267
+ // src/transport/browser-websocket.ts
1268
+ var BrowserWebSocketClient = class {
1269
+ constructor(options) {
1270
+ this.ws = null;
1271
+ this._state = "disconnected";
1272
+ this.reconnectAttempts = 0;
1273
+ this.shouldReconnect = true;
1274
+ // Queue system
1275
+ this.inboundQueue = [];
1276
+ this.outboundQueue = [];
1277
+ this.isProcessing = false;
1278
+ this.processIntervalId = null;
1279
+ // Handlers
1280
+ this.messageHandler = null;
1281
+ this.resubscribeHandler = null;
1282
+ this.stateChangeHandlers = /* @__PURE__ */ new Set();
1283
+ this.errorHandlers = /* @__PURE__ */ new Set();
1284
+ this.url = options.url;
1285
+ this.reconnectInterval = options.reconnectInterval ?? 1e3;
1286
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
1287
+ this.queueProcessInterval = options.queueProcessInterval ?? 1e3;
1288
+ }
1289
+ get state() {
1290
+ return this._state;
1291
+ }
1292
+ get isConnected() {
1293
+ return this._state === "connected";
1294
+ }
1295
+ setState(newState) {
1296
+ if (this._state !== newState) {
1297
+ this._state = newState;
1298
+ this.stateChangeHandlers.forEach((handler) => {
1299
+ try {
1300
+ handler(newState);
1301
+ } catch (error) {
1302
+ console.error("Error in state change handler:", error);
1303
+ }
1304
+ });
1305
+ }
1306
+ }
1307
+ emitError(error) {
1308
+ this.errorHandlers.forEach((handler) => {
1309
+ try {
1310
+ handler(error);
1311
+ } catch (e) {
1312
+ console.error("Error in error handler:", e);
1313
+ }
1314
+ });
1315
+ }
1316
+ async connect() {
1317
+ if (this._state === "connected" || this._state === "connecting") {
1318
+ return;
1319
+ }
1320
+ this.shouldReconnect = true;
1321
+ return this.doConnect();
1322
+ }
1323
+ doConnect() {
1324
+ return new Promise((resolve, reject) => {
1325
+ this.setState("connecting");
1326
+ try {
1327
+ this.ws = new WebSocket(this.url);
1328
+ this.ws.onopen = () => {
1329
+ this.reconnectAttempts = 0;
1330
+ this.setState("connected");
1331
+ this.startQueueProcessing();
1332
+ if (this.resubscribeHandler) {
1333
+ this.resubscribeHandler().catch((error) => {
1334
+ console.error("Error in resubscribe handler:", error);
1335
+ });
1336
+ }
1337
+ this.processQueues();
1338
+ resolve();
1339
+ };
1340
+ this.ws.onclose = () => {
1341
+ this.setState("disconnected");
1342
+ this.stopQueueProcessing();
1343
+ if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
1344
+ this.reconnectAttempts++;
1345
+ this.setState("reconnecting");
1346
+ setTimeout(() => this.doConnect(), this.reconnectInterval);
1347
+ }
1348
+ };
1349
+ this.ws.onerror = (event) => {
1350
+ const error = new Error("WebSocket error");
1351
+ this.emitError(error);
1352
+ reject(error);
1353
+ };
1354
+ this.ws.onmessage = (event) => {
1355
+ try {
1356
+ const message = JSON.parse(event.data);
1357
+ this.inboundQueue.push(message);
1358
+ this.processQueues();
1359
+ } catch (error) {
1360
+ console.error("Failed to parse WebSocket message:", error);
1361
+ }
1362
+ };
1363
+ } catch (error) {
1364
+ this.setState("disconnected");
1365
+ reject(error);
1366
+ }
1367
+ });
1368
+ }
1369
+ disconnect() {
1370
+ this.shouldReconnect = false;
1371
+ this.stopQueueProcessing();
1372
+ if (this.ws) {
1373
+ this.ws.close();
1374
+ this.ws = null;
1375
+ }
1376
+ this.setState("disconnected");
1377
+ }
1378
+ async send(message) {
1379
+ if (this.ws?.readyState === WebSocket.OPEN) {
1380
+ this.ws.send(message);
1381
+ } else {
1382
+ this.outboundQueue.push(async () => [message]);
1383
+ }
1384
+ }
1385
+ enqueueOutbound(prepareMessage) {
1386
+ this.outboundQueue.push(prepareMessage);
1387
+ this.processQueues();
1388
+ }
1389
+ async subscribe(inboxAddresses) {
1390
+ const message = JSON.stringify({
1391
+ type: "listen",
1392
+ inbox_addresses: inboxAddresses
1393
+ });
1394
+ await this.send(message);
1395
+ }
1396
+ async unsubscribe(inboxAddresses) {
1397
+ const message = JSON.stringify({
1398
+ type: "unlisten",
1399
+ inbox_addresses: inboxAddresses
1400
+ });
1401
+ await this.send(message);
1402
+ }
1403
+ setMessageHandler(handler) {
1404
+ this.messageHandler = handler;
1405
+ this.processQueues();
1406
+ }
1407
+ setResubscribeHandler(handler) {
1408
+ this.resubscribeHandler = handler;
1409
+ }
1410
+ onStateChange(handler) {
1411
+ this.stateChangeHandlers.add(handler);
1412
+ return () => {
1413
+ this.stateChangeHandlers.delete(handler);
1414
+ };
1415
+ }
1416
+ onError(handler) {
1417
+ this.errorHandlers.add(handler);
1418
+ return () => {
1419
+ this.errorHandlers.delete(handler);
1420
+ };
1421
+ }
1422
+ startQueueProcessing() {
1423
+ if (this.processIntervalId === null) {
1424
+ this.processIntervalId = setInterval(() => {
1425
+ this.processQueues();
1426
+ }, this.queueProcessInterval);
1427
+ }
1428
+ }
1429
+ stopQueueProcessing() {
1430
+ if (this.processIntervalId !== null) {
1431
+ clearInterval(this.processIntervalId);
1432
+ this.processIntervalId = null;
1433
+ }
1434
+ }
1435
+ async processQueues() {
1436
+ if (this.isProcessing) {
1437
+ return;
1438
+ }
1439
+ this.isProcessing = true;
1440
+ try {
1441
+ if (this.messageHandler) {
1442
+ const inboxMap = /* @__PURE__ */ new Map();
1443
+ while (this.inboundQueue.length > 0) {
1444
+ const message = this.inboundQueue.shift();
1445
+ const existing = inboxMap.get(message.inboxAddress) || [];
1446
+ existing.push(message);
1447
+ inboxMap.set(message.inboxAddress, existing);
1448
+ }
1449
+ const promises = [];
1450
+ for (const [_, messages] of inboxMap) {
1451
+ promises.push(
1452
+ (async () => {
1453
+ for (const message of messages) {
1454
+ try {
1455
+ await this.messageHandler(message);
1456
+ } catch (error) {
1457
+ console.error("Error processing inbound message:", error);
1458
+ }
1459
+ }
1460
+ })()
1461
+ );
1462
+ }
1463
+ await Promise.allSettled(promises);
1464
+ }
1465
+ if (this.ws?.readyState === WebSocket.OPEN) {
1466
+ while (this.outboundQueue.length > 0) {
1467
+ const prepareMessage = this.outboundQueue.shift();
1468
+ try {
1469
+ const messages = await prepareMessage();
1470
+ for (const m of messages) {
1471
+ this.ws.send(m);
1472
+ }
1473
+ } catch (error) {
1474
+ console.error("Error processing outbound message:", error);
1475
+ }
1476
+ }
1477
+ }
1478
+ } finally {
1479
+ this.isProcessing = false;
1480
+ }
1481
+ }
1482
+ };
1483
+ function createBrowserWebSocketClient(options) {
1484
+ return new BrowserWebSocketClient(options);
1485
+ }
1486
+
1487
+ // src/transport/rn-websocket.ts
1488
+ var RNWebSocketClient = class {
1489
+ constructor(options) {
1490
+ this.ws = null;
1491
+ this._state = "disconnected";
1492
+ this.reconnectAttempts = 0;
1493
+ this.shouldReconnect = true;
1494
+ // Queue system
1495
+ this.inboundQueue = [];
1496
+ this.outboundQueue = [];
1497
+ this.isProcessing = false;
1498
+ this.processIntervalId = null;
1499
+ // Handlers
1500
+ this.messageHandler = null;
1501
+ this.resubscribeHandler = null;
1502
+ this.stateChangeHandlers = /* @__PURE__ */ new Set();
1503
+ this.errorHandlers = /* @__PURE__ */ new Set();
1504
+ this.url = options.url;
1505
+ this.reconnectInterval = options.reconnectInterval ?? 1e3;
1506
+ this.maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
1507
+ this.queueProcessInterval = options.queueProcessInterval ?? 1e3;
1508
+ }
1509
+ get state() {
1510
+ return this._state;
1511
+ }
1512
+ get isConnected() {
1513
+ return this._state === "connected";
1514
+ }
1515
+ setState(newState) {
1516
+ if (this._state !== newState) {
1517
+ this._state = newState;
1518
+ this.stateChangeHandlers.forEach((handler) => {
1519
+ try {
1520
+ handler(newState);
1521
+ } catch (error) {
1522
+ console.error("Error in state change handler:", error);
1523
+ }
1524
+ });
1525
+ }
1526
+ }
1527
+ emitError(error) {
1528
+ this.errorHandlers.forEach((handler) => {
1529
+ try {
1530
+ handler(error);
1531
+ } catch (e) {
1532
+ console.error("Error in error handler:", e);
1533
+ }
1534
+ });
1535
+ }
1536
+ async connect() {
1537
+ if (this._state === "connected" || this._state === "connecting") {
1538
+ return;
1539
+ }
1540
+ this.shouldReconnect = true;
1541
+ return this.doConnect();
1542
+ }
1543
+ doConnect() {
1544
+ return new Promise((resolve, reject) => {
1545
+ this.setState("connecting");
1546
+ try {
1547
+ this.ws = new WebSocket(this.url);
1548
+ this.ws.onopen = () => {
1549
+ this.reconnectAttempts = 0;
1550
+ this.setState("connected");
1551
+ this.startQueueProcessing();
1552
+ if (this.resubscribeHandler) {
1553
+ this.resubscribeHandler().catch((error) => {
1554
+ console.error("Error in resubscribe handler:", error);
1555
+ });
1556
+ }
1557
+ this.processQueues();
1558
+ resolve();
1559
+ };
1560
+ this.ws.onclose = () => {
1561
+ this.setState("disconnected");
1562
+ this.stopQueueProcessing();
1563
+ if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
1564
+ this.reconnectAttempts++;
1565
+ this.setState("reconnecting");
1566
+ setTimeout(() => this.doConnect(), this.reconnectInterval);
1567
+ }
1568
+ };
1569
+ this.ws.onerror = () => {
1570
+ const error = new Error("WebSocket error");
1571
+ this.emitError(error);
1572
+ reject(error);
1573
+ };
1574
+ this.ws.onmessage = (event) => {
1575
+ try {
1576
+ logger.debug("[WS-RN] Raw message received:", event.data?.substring(0, 200));
1577
+ if (!event.data || !event.data.startsWith("{")) {
1578
+ logger.debug("[WS-RN] Ignoring non-JSON message");
1579
+ return;
1580
+ }
1581
+ const message = JSON.parse(event.data);
1582
+ if (!message.inboxAddress && !message.encryptedContent) {
1583
+ logger.debug("[WS-RN] Ignoring message without expected fields:", Object.keys(message));
1584
+ return;
1585
+ }
1586
+ this.inboundQueue.push(message);
1587
+ this.processQueues();
1588
+ } catch (error) {
1589
+ console.error("Failed to parse WebSocket message:", error, "raw:", event.data?.substring(0, 100));
1590
+ }
1591
+ };
1592
+ } catch (error) {
1593
+ this.setState("disconnected");
1594
+ reject(error);
1595
+ }
1596
+ });
1597
+ }
1598
+ disconnect() {
1599
+ this.shouldReconnect = false;
1600
+ this.stopQueueProcessing();
1601
+ if (this.ws) {
1602
+ this.ws.close();
1603
+ this.ws = null;
1604
+ }
1605
+ this.setState("disconnected");
1606
+ }
1607
+ async send(message) {
1608
+ if (this.ws?.readyState === WebSocket.OPEN) {
1609
+ this.ws.send(message);
1610
+ } else {
1611
+ this.outboundQueue.push(async () => [message]);
1612
+ }
1613
+ }
1614
+ enqueueOutbound(prepareMessage) {
1615
+ this.outboundQueue.push(prepareMessage);
1616
+ this.processQueues();
1617
+ }
1618
+ async subscribe(inboxAddresses) {
1619
+ const message = JSON.stringify({
1620
+ type: "listen",
1621
+ inbox_addresses: inboxAddresses
1622
+ });
1623
+ await this.send(message);
1624
+ }
1625
+ async unsubscribe(inboxAddresses) {
1626
+ const message = JSON.stringify({
1627
+ type: "unlisten",
1628
+ inbox_addresses: inboxAddresses
1629
+ });
1630
+ await this.send(message);
1631
+ }
1632
+ setMessageHandler(handler) {
1633
+ this.messageHandler = handler;
1634
+ this.processQueues();
1635
+ }
1636
+ setResubscribeHandler(handler) {
1637
+ this.resubscribeHandler = handler;
1638
+ }
1639
+ onStateChange(handler) {
1640
+ this.stateChangeHandlers.add(handler);
1641
+ return () => {
1642
+ this.stateChangeHandlers.delete(handler);
1643
+ };
1644
+ }
1645
+ onError(handler) {
1646
+ this.errorHandlers.add(handler);
1647
+ return () => {
1648
+ this.errorHandlers.delete(handler);
1649
+ };
1650
+ }
1651
+ startQueueProcessing() {
1652
+ if (this.processIntervalId === null) {
1653
+ this.processIntervalId = setInterval(() => {
1654
+ this.processQueues();
1655
+ }, this.queueProcessInterval);
1656
+ }
1657
+ }
1658
+ stopQueueProcessing() {
1659
+ if (this.processIntervalId !== null) {
1660
+ clearInterval(this.processIntervalId);
1661
+ this.processIntervalId = null;
1662
+ }
1663
+ }
1664
+ async processQueues() {
1665
+ if (this.isProcessing) {
1666
+ return;
1667
+ }
1668
+ this.isProcessing = true;
1669
+ try {
1670
+ if (this.messageHandler) {
1671
+ const inboxMap = /* @__PURE__ */ new Map();
1672
+ while (this.inboundQueue.length > 0) {
1673
+ const message = this.inboundQueue.shift();
1674
+ const existing = inboxMap.get(message.inboxAddress) || [];
1675
+ existing.push(message);
1676
+ inboxMap.set(message.inboxAddress, existing);
1677
+ }
1678
+ const promises = [];
1679
+ for (const [_, messages] of inboxMap) {
1680
+ promises.push(
1681
+ (async () => {
1682
+ for (const message of messages) {
1683
+ try {
1684
+ await this.messageHandler(message);
1685
+ } catch (error) {
1686
+ console.error("Error processing inbound message:", error);
1687
+ }
1688
+ }
1689
+ })()
1690
+ );
1691
+ }
1692
+ await Promise.allSettled(promises);
1693
+ }
1694
+ if (this.ws?.readyState === WebSocket.OPEN) {
1695
+ while (this.outboundQueue.length > 0) {
1696
+ const prepareMessage = this.outboundQueue.shift();
1697
+ try {
1698
+ const messages = await prepareMessage();
1699
+ for (const m of messages) {
1700
+ this.ws.send(m);
1701
+ }
1702
+ } catch (error) {
1703
+ console.error("Error processing outbound message:", error);
1704
+ }
1705
+ }
1706
+ }
1707
+ } finally {
1708
+ this.isProcessing = false;
1709
+ }
1710
+ }
1711
+ };
1712
+ function createRNWebSocketClient(options) {
1713
+ return new RNWebSocketClient(options);
1714
+ }
1715
+
1716
+ // src/sync/types.ts
1717
+ function isSyncRequest(payload) {
1718
+ return payload.type === "sync-request";
1719
+ }
1720
+ function isSyncInfo(payload) {
1721
+ return payload.type === "sync-info";
1722
+ }
1723
+ function isSyncInitiate(payload) {
1724
+ return payload.type === "sync-initiate";
1725
+ }
1726
+ function isSyncManifest(payload) {
1727
+ return payload.type === "sync-manifest";
1728
+ }
1729
+ function isSyncDelta(payload) {
1730
+ return payload.type === "sync-delta";
1731
+ }
1732
+
1733
+ // src/sync/utils.ts
1734
+ import { sha256 } from "@noble/hashes/sha2";
1735
+ var MAX_CHUNK_SIZE = 5 * 1024 * 1024;
1736
+ var DEFAULT_SYNC_EXPIRY_MS = 3e4;
1737
+ var AGGRESSIVE_SYNC_TIMEOUT_MS = 1e3;
1738
+ function computeHash(data) {
1739
+ const hash = sha256(new TextEncoder().encode(data));
1740
+ return bytesToHex(hash);
1741
+ }
1742
+ function computeContentHash(message) {
1743
+ const content = message.content;
1744
+ let canonical = `${content.senderId}:${content.type}`;
1745
+ switch (content.type) {
1746
+ case "post":
1747
+ canonical += `:${Array.isArray(content.text) ? content.text.join("\n") : content.text}`;
1748
+ if (content.repliesToMessageId) {
1749
+ canonical += `:reply:${content.repliesToMessageId}`;
1750
+ }
1751
+ break;
1752
+ case "embed":
1753
+ canonical += `:${content.imageUrl || ""}:${content.videoUrl || ""}`;
1754
+ if (content.repliesToMessageId) {
1755
+ canonical += `:reply:${content.repliesToMessageId}`;
1756
+ }
1757
+ break;
1758
+ case "sticker":
1759
+ canonical += `:${content.stickerId}`;
1760
+ if (content.repliesToMessageId) {
1761
+ canonical += `:reply:${content.repliesToMessageId}`;
1762
+ }
1763
+ break;
1764
+ case "edit-message":
1765
+ canonical += `:${content.originalMessageId}:${Array.isArray(content.editedText) ? content.editedText.join("\n") : content.editedText}:${content.editedAt}`;
1766
+ break;
1767
+ case "remove-message":
1768
+ canonical += `:${content.removeMessageId}`;
1769
+ break;
1770
+ case "join":
1771
+ case "leave":
1772
+ case "kick":
1773
+ break;
1774
+ case "event":
1775
+ canonical += `:${content.text}`;
1776
+ break;
1777
+ case "update-profile":
1778
+ canonical += `:${content.displayName}:${content.userIcon}`;
1779
+ break;
1780
+ case "mute":
1781
+ canonical += `:${content.targetUserId}:${content.action}:${content.muteId}`;
1782
+ break;
1783
+ case "pin":
1784
+ canonical += `:${content.targetMessageId}:${content.action}`;
1785
+ break;
1786
+ case "reaction":
1787
+ case "remove-reaction":
1788
+ canonical += `:${content.messageId}:${content.reaction}`;
1789
+ break;
1790
+ case "delete-conversation":
1791
+ break;
1792
+ }
1793
+ return computeHash(canonical);
1794
+ }
1795
+ function computeReactionHash(reactions) {
1796
+ if (!reactions || reactions.length === 0) {
1797
+ return computeHash("");
1798
+ }
1799
+ const sorted = [...reactions].sort((a, b) => a.emojiId.localeCompare(b.emojiId));
1800
+ const canonical = sorted.map((r) => {
1801
+ const sortedMembers = [...r.memberIds].sort();
1802
+ return `${r.emojiId}:${sortedMembers.join(",")}`;
1803
+ }).join("|");
1804
+ return computeHash(canonical);
1805
+ }
1806
+ function computeMemberHash(member) {
1807
+ const displayNameHash = computeHash(member.display_name || "");
1808
+ const iconHash = computeHash(member.profile_image || "");
1809
+ return { displayNameHash, iconHash };
1810
+ }
1811
+ function computeManifestHash(digests) {
1812
+ if (digests.length === 0) {
1813
+ return computeHash("");
1814
+ }
1815
+ const sorted = [...digests].sort((a, b) => a.messageId.localeCompare(b.messageId));
1816
+ const ids = sorted.map((d) => d.messageId).join(":");
1817
+ return computeHash(ids);
1818
+ }
1819
+ function createMessageDigest(message) {
1820
+ return {
1821
+ messageId: message.messageId,
1822
+ createdDate: message.createdDate,
1823
+ contentHash: computeContentHash(message),
1824
+ modifiedDate: message.modifiedDate !== message.createdDate ? message.modifiedDate : void 0
1825
+ };
1826
+ }
1827
+ function createReactionDigest(messageId, reactions) {
1828
+ if (!reactions || reactions.length === 0) {
1829
+ return [];
1830
+ }
1831
+ return reactions.map((r) => ({
1832
+ messageId,
1833
+ emojiId: r.emojiId,
1834
+ count: r.count,
1835
+ membersHash: computeHash([...r.memberIds].sort().join(","))
1836
+ }));
1837
+ }
1838
+ function createManifest(spaceId, channelId, messages) {
1839
+ const sorted = [...messages].sort((a, b) => a.createdDate - b.createdDate);
1840
+ const digests = sorted.map(createMessageDigest);
1841
+ const reactionDigests = [];
1842
+ for (const msg of sorted) {
1843
+ if (msg.reactions && msg.reactions.length > 0) {
1844
+ reactionDigests.push(...createReactionDigest(msg.messageId, msg.reactions));
1845
+ }
1846
+ }
1847
+ return {
1848
+ spaceId,
1849
+ channelId,
1850
+ messageCount: messages.length,
1851
+ oldestTimestamp: sorted[0]?.createdDate || 0,
1852
+ newestTimestamp: sorted[sorted.length - 1]?.createdDate || 0,
1853
+ digests,
1854
+ reactionDigests
1855
+ };
1856
+ }
1857
+ function createMemberDigest(member) {
1858
+ const { displayNameHash, iconHash } = computeMemberHash(member);
1859
+ return {
1860
+ address: member.address,
1861
+ inboxAddress: member.inbox_address || "",
1862
+ displayNameHash,
1863
+ iconHash
1864
+ };
1865
+ }
1866
+ function computeMessageDiff(ourManifest, theirManifest) {
1867
+ const ourDigests = new Map(ourManifest.digests.map((d) => [d.messageId, d]));
1868
+ const theirDigests = new Map(theirManifest.digests.map((d) => [d.messageId, d]));
1869
+ const missingIds = [];
1870
+ const outdatedIds = [];
1871
+ const extraIds = [];
1872
+ for (const [id, theirDigest] of theirDigests) {
1873
+ const ourDigest = ourDigests.get(id);
1874
+ if (!ourDigest) {
1875
+ missingIds.push(id);
1876
+ } else if (ourDigest.contentHash !== theirDigest.contentHash) {
1877
+ const theirModified = theirDigest.modifiedDate || theirDigest.createdDate;
1878
+ const ourModified = ourDigest.modifiedDate || ourDigest.createdDate;
1879
+ if (theirModified > ourModified) {
1880
+ outdatedIds.push(id);
1881
+ }
1882
+ }
1883
+ }
1884
+ for (const [id] of ourDigests) {
1885
+ if (!theirDigests.has(id)) {
1886
+ extraIds.push(id);
1887
+ }
1888
+ }
1889
+ return { missingIds, outdatedIds, extraIds };
1890
+ }
1891
+ function computeReactionDiff(ourReactions, theirDigests) {
1892
+ const toAdd = [];
1893
+ const toRemove = [];
1894
+ const theirByMessage = /* @__PURE__ */ new Map();
1895
+ for (const digest of theirDigests) {
1896
+ const existing = theirByMessage.get(digest.messageId) || [];
1897
+ existing.push(digest);
1898
+ theirByMessage.set(digest.messageId, existing);
1899
+ }
1900
+ for (const [messageId, theirMsgDigests] of theirByMessage) {
1901
+ const ourMsgReactions = ourReactions.get(messageId) || [];
1902
+ const ourByEmoji = new Map(ourMsgReactions.map((r) => [r.emojiId, r]));
1903
+ for (const theirDigest of theirMsgDigests) {
1904
+ const ourReaction = ourByEmoji.get(theirDigest.emojiId);
1905
+ if (!ourReaction) {
1906
+ continue;
1907
+ }
1908
+ const ourMembersHash = computeHash([...ourReaction.memberIds].sort().join(","));
1909
+ if (ourMembersHash !== theirDigest.membersHash) {
1910
+ }
1911
+ }
1912
+ }
1913
+ return { toAdd, toRemove };
1914
+ }
1915
+ function computeMemberDiff(ourDigests, theirDigests) {
1916
+ const ourMap = new Map(ourDigests.map((d) => [d.address, d]));
1917
+ const theirMap = new Map(theirDigests.map((d) => [d.address, d]));
1918
+ const missingAddresses = [];
1919
+ const outdatedAddresses = [];
1920
+ const extraAddresses = [];
1921
+ for (const [address, theirDigest] of theirMap) {
1922
+ const ourDigest = ourMap.get(address);
1923
+ if (!ourDigest) {
1924
+ missingAddresses.push(address);
1925
+ } else if (ourDigest.displayNameHash !== theirDigest.displayNameHash || ourDigest.iconHash !== theirDigest.iconHash) {
1926
+ outdatedAddresses.push(address);
1927
+ }
1928
+ }
1929
+ for (const [address] of ourMap) {
1930
+ if (!theirMap.has(address)) {
1931
+ extraAddresses.push(address);
1932
+ }
1933
+ }
1934
+ return { missingAddresses, outdatedAddresses, extraAddresses };
1935
+ }
1936
+ function computePeerDiff(ourPeerIds, theirPeerIds) {
1937
+ const ourSet = new Set(ourPeerIds);
1938
+ const theirSet = new Set(theirPeerIds);
1939
+ const missingPeerIds = theirPeerIds.filter((id) => !ourSet.has(id));
1940
+ const extraPeerIds = ourPeerIds.filter((id) => !theirSet.has(id));
1941
+ return { missingPeerIds, extraPeerIds };
1942
+ }
1943
+ function buildMessageDelta(spaceId, channelId, diff, messageMap, tombstones) {
1944
+ const newMessages = diff.extraIds.map((id) => messageMap.get(id)).filter((m) => m !== void 0);
1945
+ const updatedMessages = diff.outdatedIds.map((id) => messageMap.get(id)).filter((m) => m !== void 0);
1946
+ const deletedMessageIds = tombstones.filter((t) => t.spaceId === spaceId && t.channelId === channelId).map((t) => t.messageId);
1947
+ return {
1948
+ spaceId,
1949
+ channelId,
1950
+ newMessages,
1951
+ updatedMessages,
1952
+ deletedMessageIds
1953
+ };
1954
+ }
1955
+ function buildReactionDelta(spaceId, channelId, messageMap, messageIds) {
1956
+ const added = [];
1957
+ for (const messageId of messageIds) {
1958
+ const message = messageMap.get(messageId);
1959
+ if (message?.reactions) {
1960
+ for (const reaction of message.reactions) {
1961
+ added.push({
1962
+ messageId,
1963
+ emojiId: reaction.emojiId,
1964
+ memberIds: reaction.memberIds
1965
+ });
1966
+ }
1967
+ }
1968
+ }
1969
+ return {
1970
+ spaceId,
1971
+ channelId,
1972
+ added,
1973
+ removed: []
1974
+ // Removed reactions are harder to track without explicit tombstones
1975
+ };
1976
+ }
1977
+ function buildMemberDelta(spaceId, diff, memberMap) {
1978
+ const addresses = [...diff.missingAddresses, ...diff.outdatedAddresses];
1979
+ const members = addresses.map((addr) => memberMap.get(addr)).filter((m) => m !== void 0);
1980
+ return {
1981
+ spaceId,
1982
+ members,
1983
+ removedAddresses: []
1984
+ // Would need explicit tracking
1985
+ };
1986
+ }
1987
+ function chunkMessages(messages) {
1988
+ const chunks = [];
1989
+ let currentChunk = [];
1990
+ let currentSize = 0;
1991
+ for (const msg of messages) {
1992
+ const msgSize = JSON.stringify(msg).length;
1993
+ if (msgSize > MAX_CHUNK_SIZE) {
1994
+ if (currentChunk.length > 0) {
1995
+ chunks.push(currentChunk);
1996
+ currentChunk = [];
1997
+ currentSize = 0;
1998
+ }
1999
+ chunks.push([msg]);
2000
+ continue;
2001
+ }
2002
+ if (currentSize + msgSize > MAX_CHUNK_SIZE && currentChunk.length > 0) {
2003
+ chunks.push(currentChunk);
2004
+ currentChunk = [];
2005
+ currentSize = 0;
2006
+ }
2007
+ currentChunk.push(msg);
2008
+ currentSize += msgSize;
2009
+ }
2010
+ if (currentChunk.length > 0) {
2011
+ chunks.push(currentChunk);
2012
+ }
2013
+ return chunks;
2014
+ }
2015
+ function chunkMembers(members) {
2016
+ const chunks = [];
2017
+ let currentChunk = [];
2018
+ let currentSize = 0;
2019
+ for (const member of members) {
2020
+ const memberSize = JSON.stringify(member).length;
2021
+ if (currentSize + memberSize > MAX_CHUNK_SIZE && currentChunk.length > 0) {
2022
+ chunks.push(currentChunk);
2023
+ currentChunk = [];
2024
+ currentSize = 0;
2025
+ }
2026
+ currentChunk.push(member);
2027
+ currentSize += memberSize;
2028
+ }
2029
+ if (currentChunk.length > 0) {
2030
+ chunks.push(currentChunk);
2031
+ }
2032
+ return chunks;
2033
+ }
2034
+ function createSyncSummary(messages, memberCount) {
2035
+ const digests = messages.map(createMessageDigest);
2036
+ const sorted = [...messages].sort((a, b) => a.createdDate - b.createdDate);
2037
+ return {
2038
+ memberCount,
2039
+ messageCount: messages.length,
2040
+ newestMessageTimestamp: sorted[sorted.length - 1]?.createdDate || 0,
2041
+ oldestMessageTimestamp: sorted[0]?.createdDate || 0,
2042
+ manifestHash: computeManifestHash(digests)
2043
+ };
2044
+ }
2045
+
2046
+ // src/sync/service.ts
2047
+ var SyncService = class {
2048
+ constructor(config2) {
2049
+ /** Active sync sessions by spaceId */
2050
+ this.sessions = /* @__PURE__ */ new Map();
2051
+ /** Deleted message tombstones (caller must persist these) */
2052
+ this.tombstones = [];
2053
+ /** Pre-computed sync payload cache per space:channel - always ready to use */
2054
+ this.payloadCache = /* @__PURE__ */ new Map();
2055
+ this.storage = config2.storage;
2056
+ this.maxMessages = config2.maxMessages ?? 1e3;
2057
+ this.requestExpiry = config2.requestExpiry ?? DEFAULT_SYNC_EXPIRY_MS;
2058
+ this.onInitiateSync = config2.onInitiateSync;
2059
+ }
2060
+ // ============ Payload Cache Management ============
2061
+ /**
2062
+ * Get cache key for space/channel
2063
+ */
2064
+ getCacheKey(spaceId, channelId) {
2065
+ return `${spaceId}:${channelId}`;
2066
+ }
2067
+ /**
2068
+ * Get or initialize the payload cache for a space/channel.
2069
+ * If not cached, loads from storage and builds the payload once.
2070
+ */
2071
+ async getPayloadCache(spaceId, channelId) {
2072
+ const key = this.getCacheKey(spaceId, channelId);
2073
+ const cached = this.payloadCache.get(key);
2074
+ if (cached) {
2075
+ logger.log(`[SyncService] Using cached payload for ${spaceId.substring(0, 12)}:${channelId.substring(0, 12)}`);
2076
+ return cached;
2077
+ }
2078
+ logger.log(`[SyncService] Building initial payload cache for ${spaceId.substring(0, 12)}:${channelId.substring(0, 12)}`);
2079
+ const messages = await this.getChannelMessages(spaceId, channelId);
2080
+ const members = await this.storage.getSpaceMembers(spaceId);
2081
+ const payload = this.buildPayloadCache(spaceId, channelId, messages, members);
2082
+ this.payloadCache.set(key, payload);
2083
+ return payload;
2084
+ }
2085
+ /**
2086
+ * Build the payload cache from messages and members
2087
+ */
2088
+ buildPayloadCache(spaceId, channelId, messages, members) {
2089
+ const messageMap = new Map(messages.map((m) => [m.messageId, m]));
2090
+ const memberMap = new Map(members.map((m) => [m.address, m]));
2091
+ const manifest = createManifest(spaceId, channelId, messages);
2092
+ const memberDigests = members.map(createMemberDigest);
2093
+ const summary = createSyncSummary(messages, members.length);
2094
+ return { manifest, memberDigests, summary, messageMap, memberMap };
2095
+ }
2096
+ /**
2097
+ * Rebuild derived fields (manifest, digests, summary) from the maps
2098
+ */
2099
+ rebuildPayloadCache(spaceId, channelId, cache) {
2100
+ const messages = [...cache.messageMap.values()];
2101
+ const members = [...cache.memberMap.values()];
2102
+ cache.manifest = createManifest(spaceId, channelId, messages);
2103
+ cache.memberDigests = members.map(createMemberDigest);
2104
+ cache.summary = createSyncSummary(messages, members.length);
2105
+ }
2106
+ /**
2107
+ * Invalidate cache for a space/channel (forces reload from storage on next access)
2108
+ */
2109
+ invalidateCache(spaceId, channelId) {
2110
+ if (channelId) {
2111
+ const key = this.getCacheKey(spaceId, channelId);
2112
+ this.payloadCache.delete(key);
2113
+ logger.log(`[SyncService] Invalidated cache for ${spaceId.substring(0, 12)}:${channelId.substring(0, 12)}`);
2114
+ } else {
2115
+ for (const key of this.payloadCache.keys()) {
2116
+ if (key.startsWith(`${spaceId}:`)) {
2117
+ this.payloadCache.delete(key);
2118
+ }
2119
+ }
2120
+ logger.log(`[SyncService] Invalidated all caches for space ${spaceId.substring(0, 12)}`);
2121
+ }
2122
+ }
2123
+ /**
2124
+ * Update cache with a new/updated message (incremental update - no storage query)
2125
+ */
2126
+ updateCacheWithMessage(spaceId, channelId, message) {
2127
+ const key = this.getCacheKey(spaceId, channelId);
2128
+ const cached = this.payloadCache.get(key);
2129
+ if (cached) {
2130
+ cached.messageMap.set(message.messageId, message);
2131
+ this.rebuildPayloadCache(spaceId, channelId, cached);
2132
+ logger.log(`[SyncService] Updated cache with message ${message.messageId.substring(0, 12)}`);
2133
+ }
2134
+ }
2135
+ /**
2136
+ * Remove a message from cache (incremental update - no storage query)
2137
+ */
2138
+ removeCacheMessage(spaceId, channelId, messageId) {
2139
+ const key = this.getCacheKey(spaceId, channelId);
2140
+ const cached = this.payloadCache.get(key);
2141
+ if (cached) {
2142
+ cached.messageMap.delete(messageId);
2143
+ this.rebuildPayloadCache(spaceId, channelId, cached);
2144
+ logger.log(`[SyncService] Removed message ${messageId.substring(0, 12)} from cache`);
2145
+ }
2146
+ }
2147
+ /**
2148
+ * Update cache with a new/updated member (incremental update - no storage query)
2149
+ */
2150
+ updateCacheWithMember(spaceId, channelId, member) {
2151
+ const key = this.getCacheKey(spaceId, channelId);
2152
+ const cached = this.payloadCache.get(key);
2153
+ if (cached) {
2154
+ cached.memberMap.set(member.address, member);
2155
+ this.rebuildPayloadCache(spaceId, channelId, cached);
2156
+ logger.log(`[SyncService] Updated cache with member ${member.address.substring(0, 12)}`);
2157
+ }
2158
+ }
2159
+ /**
2160
+ * Check if cache exists for a space/channel
2161
+ */
2162
+ hasCachedPayload(spaceId, channelId) {
2163
+ return this.payloadCache.has(this.getCacheKey(spaceId, channelId));
2164
+ }
2165
+ // ============ Session Management ============
2166
+ /**
2167
+ * Check if a sync session is active for a space
2168
+ */
2169
+ hasActiveSession(spaceId) {
2170
+ const session = this.sessions.get(spaceId);
2171
+ if (!session) return false;
2172
+ if (Date.now() > session.expiry) {
2173
+ this.sessions.delete(spaceId);
2174
+ return false;
2175
+ }
2176
+ return true;
2177
+ }
2178
+ /**
2179
+ * Check if sync is in progress for a space
2180
+ */
2181
+ isSyncInProgress(spaceId) {
2182
+ const session = this.sessions.get(spaceId);
2183
+ return session?.inProgress ?? false;
2184
+ }
2185
+ /**
2186
+ * Mark sync as in progress
2187
+ */
2188
+ setSyncInProgress(spaceId, inProgress) {
2189
+ const session = this.sessions.get(spaceId);
2190
+ if (session) {
2191
+ session.inProgress = inProgress;
2192
+ }
2193
+ }
2194
+ /**
2195
+ * Set the sync target (who we're syncing with)
2196
+ */
2197
+ setSyncTarget(spaceId, targetInbox) {
2198
+ const session = this.sessions.get(spaceId);
2199
+ if (session) {
2200
+ session.syncTarget = targetInbox;
2201
+ }
2202
+ }
2203
+ /**
2204
+ * Get the sync target for a space
2205
+ */
2206
+ getSyncTarget(spaceId) {
2207
+ const session = this.sessions.get(spaceId);
2208
+ return session?.syncTarget;
2209
+ }
2210
+ // ============ Step 1: Sync Request ============
2211
+ /**
2212
+ * Build sync-request payload to broadcast via hub
2213
+ */
2214
+ async buildSyncRequest(spaceId, channelId, inboxAddress) {
2215
+ const cache = await this.getPayloadCache(spaceId, channelId);
2216
+ const expiry = Date.now() + this.requestExpiry;
2217
+ this.sessions.set(spaceId, {
2218
+ spaceId,
2219
+ channelId,
2220
+ expiry,
2221
+ candidates: [],
2222
+ inProgress: false
2223
+ });
2224
+ return {
2225
+ type: "sync-request",
2226
+ inboxAddress,
2227
+ expiry,
2228
+ summary: cache.summary
2229
+ };
2230
+ }
2231
+ /**
2232
+ * Schedule sync initiation after timeout
2233
+ */
2234
+ scheduleSyncInitiation(spaceId, callback, timeoutMs = this.requestExpiry) {
2235
+ const session = this.sessions.get(spaceId);
2236
+ if (!session) return;
2237
+ if (session.timeout) {
2238
+ clearTimeout(session.timeout);
2239
+ }
2240
+ session.timeout = setTimeout(() => {
2241
+ callback();
2242
+ }, timeoutMs);
2243
+ }
2244
+ // ============ Step 2: Sync Info ============
2245
+ /**
2246
+ * Build sync-info response if we have useful data.
2247
+ * Returns null if we have nothing to offer or are already in sync.
2248
+ */
2249
+ async buildSyncInfo(spaceId, channelId, inboxAddress, theirSummary) {
2250
+ logger.log(`[SyncService] buildSyncInfo called for space=${spaceId.substring(0, 12)}, channel=${channelId.substring(0, 12)}`);
2251
+ const cache = await this.getPayloadCache(spaceId, channelId);
2252
+ const ourSummary = cache.summary;
2253
+ logger.log(`[SyncService] buildSyncInfo: our data - ${cache.messageMap.size} messages, ${cache.memberMap.size} members`);
2254
+ logger.log(`[SyncService] buildSyncInfo: their summary:`, theirSummary);
2255
+ if (cache.messageMap.size === 0 && cache.memberMap.size === 0) {
2256
+ logger.log(`[SyncService] buildSyncInfo: returning null - we have no data`);
2257
+ return null;
2258
+ }
2259
+ logger.log(`[SyncService] buildSyncInfo: our summary:`, ourSummary);
2260
+ if (ourSummary.manifestHash === theirSummary.manifestHash && ourSummary.memberCount === theirSummary.memberCount) {
2261
+ logger.log(`[SyncService] buildSyncInfo: returning null - hashes and member counts match`);
2262
+ return null;
2263
+ }
2264
+ const hasMoreMessages = ourSummary.messageCount > theirSummary.messageCount;
2265
+ const hasMoreMembers = ourSummary.memberCount > theirSummary.memberCount;
2266
+ const hasNewerMessages = ourSummary.newestMessageTimestamp > theirSummary.newestMessageTimestamp;
2267
+ const hasOlderMessages = ourSummary.oldestMessageTimestamp < theirSummary.oldestMessageTimestamp;
2268
+ const hasDifferentMessages = ourSummary.manifestHash !== theirSummary.manifestHash;
2269
+ logger.log(`[SyncService] buildSyncInfo: comparison - hasMoreMessages=${hasMoreMessages}, hasMoreMembers=${hasMoreMembers}, hasNewerMessages=${hasNewerMessages}, hasOlderMessages=${hasOlderMessages}, hasDifferentMessages=${hasDifferentMessages}`);
2270
+ if (!hasMoreMessages && !hasMoreMembers && !hasNewerMessages && !hasOlderMessages && !hasDifferentMessages) {
2271
+ logger.log(`[SyncService] buildSyncInfo: returning null - they have same or more data`);
2272
+ return null;
2273
+ }
2274
+ logger.log(`[SyncService] buildSyncInfo: returning sync-info response - we have data they don't`);
2275
+ return {
2276
+ type: "sync-info",
2277
+ inboxAddress,
2278
+ summary: ourSummary
2279
+ };
2280
+ }
2281
+ /**
2282
+ * Add candidate from sync-info response
2283
+ */
2284
+ addCandidate(spaceId, candidate) {
2285
+ logger.log(`[SyncService] addCandidate called for space ${spaceId.substring(0, 12)}, candidate inbox: ${candidate.inboxAddress?.substring(0, 12)}`);
2286
+ const session = this.sessions.get(spaceId);
2287
+ if (!session) {
2288
+ logger.log(`[SyncService] addCandidate: No session found for space`);
2289
+ return;
2290
+ }
2291
+ if (Date.now() > session.expiry) {
2292
+ logger.log(`[SyncService] addCandidate: Session expired`);
2293
+ return;
2294
+ }
2295
+ session.candidates.push(candidate);
2296
+ logger.log(`[SyncService] addCandidate: Now have ${session.candidates.length} candidates`);
2297
+ if (session.candidates.length === 1 && this.onInitiateSync) {
2298
+ logger.log(`[SyncService] addCandidate: Scheduling sync initiation in ${AGGRESSIVE_SYNC_TIMEOUT_MS}ms`);
2299
+ this.scheduleSyncInitiation(
2300
+ spaceId,
2301
+ () => {
2302
+ const best = this.selectBestCandidate(spaceId);
2303
+ logger.log(`[SyncService] addCandidate: Timeout triggered, best candidate: ${best?.inboxAddress?.substring(0, 12) || "none"}`);
2304
+ if (best?.inboxAddress) {
2305
+ this.onInitiateSync(spaceId, best.inboxAddress);
2306
+ } else {
2307
+ logger.log(`[SyncService] addCandidate: No valid candidate to sync with`);
2308
+ }
2309
+ },
2310
+ AGGRESSIVE_SYNC_TIMEOUT_MS
2311
+ );
2312
+ }
2313
+ }
2314
+ /**
2315
+ * Select best candidate based on data availability
2316
+ */
2317
+ selectBestCandidate(spaceId) {
2318
+ const session = this.sessions.get(spaceId);
2319
+ if (!session || session.candidates.length === 0) return null;
2320
+ const sorted = [...session.candidates].sort((a, b) => {
2321
+ const msgDiff = b.summary.messageCount - a.summary.messageCount;
2322
+ if (msgDiff !== 0) return msgDiff;
2323
+ return b.summary.memberCount - a.summary.memberCount;
2324
+ });
2325
+ return sorted[0];
2326
+ }
2327
+ // ============ Step 3: Sync Initiate ============
2328
+ /**
2329
+ * Build sync-initiate payload for selected peer
2330
+ */
2331
+ async buildSyncInitiate(spaceId, channelId, inboxAddress, peerIds) {
2332
+ const candidate = this.selectBestCandidate(spaceId);
2333
+ if (!candidate) {
2334
+ this.sessions.delete(spaceId);
2335
+ return null;
2336
+ }
2337
+ const cache = await this.getPayloadCache(spaceId, channelId);
2338
+ this.setSyncInProgress(spaceId, true);
2339
+ this.setSyncTarget(spaceId, candidate.inboxAddress);
2340
+ return {
2341
+ target: candidate.inboxAddress,
2342
+ payload: {
2343
+ type: "sync-initiate",
2344
+ inboxAddress,
2345
+ manifest: cache.manifest,
2346
+ memberDigests: cache.memberDigests,
2347
+ peerIds
2348
+ }
2349
+ };
2350
+ }
2351
+ // ============ Step 4: Sync Manifest ============
2352
+ /**
2353
+ * Build sync-manifest response to sync-initiate
2354
+ */
2355
+ async buildSyncManifest(spaceId, channelId, peerIds, inboxAddress) {
2356
+ const cache = await this.getPayloadCache(spaceId, channelId);
2357
+ return {
2358
+ type: "sync-manifest",
2359
+ inboxAddress,
2360
+ manifest: cache.manifest,
2361
+ memberDigests: cache.memberDigests,
2362
+ peerIds
2363
+ };
2364
+ }
2365
+ // ============ Step 5: Sync Delta ============
2366
+ /**
2367
+ * Build sync-delta payloads based on manifest comparison.
2368
+ * May return multiple payloads for chunking.
2369
+ */
2370
+ async buildSyncDelta(spaceId, channelId, theirManifest, theirMemberDigests, theirPeerIds, ourPeerEntries) {
2371
+ const cache = await this.getPayloadCache(spaceId, channelId);
2372
+ const ourPeerIds = [...ourPeerEntries.keys()];
2373
+ const messageDiff = computeMessageDiff(cache.manifest, theirManifest);
2374
+ const memberDiff = computeMemberDiff(theirMemberDigests, cache.memberDigests);
2375
+ const peerDiff = computePeerDiff(theirPeerIds, ourPeerIds);
2376
+ const messageDelta = buildMessageDelta(
2377
+ spaceId,
2378
+ channelId,
2379
+ messageDiff,
2380
+ cache.messageMap,
2381
+ this.tombstones
2382
+ );
2383
+ const reactionMessageIds = [...messageDiff.extraIds, ...messageDiff.outdatedIds];
2384
+ const reactionDelta = buildReactionDelta(spaceId, channelId, cache.messageMap, reactionMessageIds);
2385
+ const memberDelta = buildMemberDelta(spaceId, memberDiff, cache.memberMap);
2386
+ const peerMapDelta = {
2387
+ spaceId,
2388
+ added: peerDiff.extraPeerIds.map((id) => ourPeerEntries.get(id)).filter((e) => e !== void 0),
2389
+ updated: [],
2390
+ removed: []
2391
+ };
2392
+ const payloads = [];
2393
+ const allMessages = [...messageDelta.newMessages, ...messageDelta.updatedMessages];
2394
+ if (allMessages.length > 0) {
2395
+ const chunks = chunkMessages(allMessages);
2396
+ for (let i = 0; i < chunks.length; i++) {
2397
+ const chunk = chunks[i];
2398
+ const isLast = i === chunks.length - 1;
2399
+ const chunkDelta = {
2400
+ spaceId,
2401
+ channelId,
2402
+ newMessages: chunk.filter(
2403
+ (m) => messageDiff.extraIds.includes(m.messageId)
2404
+ ),
2405
+ updatedMessages: chunk.filter(
2406
+ (m) => messageDiff.outdatedIds.includes(m.messageId)
2407
+ ),
2408
+ deletedMessageIds: isLast ? messageDelta.deletedMessageIds : []
2409
+ };
2410
+ payloads.push({
2411
+ type: "sync-delta",
2412
+ messageDelta: chunkDelta,
2413
+ // Include reaction delta only in last chunk
2414
+ reactionDelta: isLast && reactionDelta.added.length > 0 ? reactionDelta : void 0,
2415
+ isFinal: false
2416
+ });
2417
+ }
2418
+ }
2419
+ if (memberDelta.members.length > 0 || peerMapDelta.added.length > 0 || allMessages.length === 0) {
2420
+ payloads.push({
2421
+ type: "sync-delta",
2422
+ memberDelta: memberDelta.members.length > 0 ? memberDelta : void 0,
2423
+ peerMapDelta: peerMapDelta.added.length > 0 ? peerMapDelta : void 0,
2424
+ isFinal: true
2425
+ });
2426
+ } else if (payloads.length > 0) {
2427
+ payloads[payloads.length - 1].isFinal = true;
2428
+ }
2429
+ if (payloads.length === 0) {
2430
+ payloads.push({
2431
+ type: "sync-delta",
2432
+ isFinal: true
2433
+ });
2434
+ }
2435
+ return payloads;
2436
+ }
2437
+ // ============ Delta Application ============
2438
+ /**
2439
+ * Apply received message delta to local storage
2440
+ */
2441
+ async applyMessageDelta(delta) {
2442
+ for (const msg of delta.newMessages) {
2443
+ await this.storage.saveMessage(msg, msg.createdDate, "", "", "", "");
2444
+ }
2445
+ for (const msg of delta.updatedMessages) {
2446
+ await this.storage.saveMessage(msg, msg.createdDate, "", "", "", "");
2447
+ }
2448
+ for (const id of delta.deletedMessageIds) {
2449
+ await this.storage.deleteMessage(id);
2450
+ }
2451
+ }
2452
+ /**
2453
+ * Apply received reaction delta to local storage.
2454
+ * This updates the reactions on existing messages.
2455
+ */
2456
+ async applyReactionDelta(delta) {
2457
+ for (const addition of delta.added) {
2458
+ const message = await this.storage.getMessage({
2459
+ spaceId: delta.spaceId,
2460
+ channelId: delta.channelId,
2461
+ messageId: addition.messageId
2462
+ });
2463
+ if (message) {
2464
+ const reactions = message.reactions || [];
2465
+ const existing = reactions.find((r) => r.emojiId === addition.emojiId);
2466
+ if (existing) {
2467
+ const allMembers = /* @__PURE__ */ new Set([...existing.memberIds, ...addition.memberIds]);
2468
+ existing.memberIds = [...allMembers];
2469
+ existing.count = existing.memberIds.length;
2470
+ } else {
2471
+ reactions.push({
2472
+ emojiId: addition.emojiId,
2473
+ emojiName: addition.emojiId,
2474
+ spaceId: delta.spaceId,
2475
+ count: addition.memberIds.length,
2476
+ memberIds: addition.memberIds
2477
+ });
2478
+ }
2479
+ message.reactions = reactions;
2480
+ await this.storage.saveMessage(message, message.createdDate, "", "", "", "");
2481
+ }
2482
+ }
2483
+ for (const removal of delta.removed) {
2484
+ const message = await this.storage.getMessage({
2485
+ spaceId: delta.spaceId,
2486
+ channelId: delta.channelId,
2487
+ messageId: removal.messageId
2488
+ });
2489
+ if (message) {
2490
+ const reactions = message.reactions || [];
2491
+ const existing = reactions.find((r) => r.emojiId === removal.emojiId);
2492
+ if (existing) {
2493
+ existing.memberIds = existing.memberIds.filter(
2494
+ (id) => !removal.memberIds.includes(id)
2495
+ );
2496
+ existing.count = existing.memberIds.length;
2497
+ if (existing.memberIds.length === 0) {
2498
+ message.reactions = reactions.filter((r) => r.emojiId !== removal.emojiId);
2499
+ }
2500
+ }
2501
+ await this.storage.saveMessage(message, message.createdDate, "", "", "", "");
2502
+ }
2503
+ }
2504
+ }
2505
+ /**
2506
+ * Apply received member delta to local storage
2507
+ */
2508
+ async applyMemberDelta(delta) {
2509
+ for (const member of delta.members) {
2510
+ await this.storage.saveSpaceMember(delta.spaceId, member);
2511
+ }
2512
+ }
2513
+ /**
2514
+ * Apply full sync delta
2515
+ */
2516
+ async applySyncDelta(delta) {
2517
+ if (delta.messageDelta) {
2518
+ await this.applyMessageDelta(delta.messageDelta);
2519
+ }
2520
+ if (delta.reactionDelta) {
2521
+ await this.applyReactionDelta(delta.reactionDelta);
2522
+ }
2523
+ if (delta.memberDelta) {
2524
+ await this.applyMemberDelta(delta.memberDelta);
2525
+ }
2526
+ if (delta.isFinal) {
2527
+ const spaceId = delta.messageDelta?.spaceId || delta.memberDelta?.spaceId;
2528
+ if (spaceId) {
2529
+ this.sessions.delete(spaceId);
2530
+ }
2531
+ }
2532
+ }
2533
+ // ============ Tombstone Management ============
2534
+ /**
2535
+ * Record a deleted message tombstone
2536
+ */
2537
+ addTombstone(tombstone) {
2538
+ this.tombstones.push(tombstone);
2539
+ }
2540
+ /**
2541
+ * Get all tombstones (for persistence by caller)
2542
+ */
2543
+ getTombstones() {
2544
+ return [...this.tombstones];
2545
+ }
2546
+ /**
2547
+ * Load tombstones (from caller's persistence)
2548
+ */
2549
+ loadTombstones(tombstones) {
2550
+ this.tombstones = [...tombstones];
2551
+ }
2552
+ /**
2553
+ * Clean up old tombstones (older than 30 days)
2554
+ */
2555
+ cleanupTombstones(maxAgeMs = 30 * 24 * 60 * 60 * 1e3) {
2556
+ const cutoff = Date.now() - maxAgeMs;
2557
+ this.tombstones = this.tombstones.filter((t) => t.deletedAt > cutoff);
2558
+ }
2559
+ // ============ Helpers ============
2560
+ async getChannelMessages(spaceId, channelId) {
2561
+ const result = await this.storage.getMessages({
2562
+ spaceId,
2563
+ channelId,
2564
+ limit: this.maxMessages
2565
+ });
2566
+ return result.messages;
2567
+ }
2568
+ /**
2569
+ * Clean up expired sessions
2570
+ */
2571
+ cleanupSessions() {
2572
+ const now = Date.now();
2573
+ for (const [spaceId, session] of this.sessions) {
2574
+ if (now > session.expiry) {
2575
+ if (session.timeout) {
2576
+ clearTimeout(session.timeout);
2577
+ }
2578
+ this.sessions.delete(spaceId);
2579
+ }
2580
+ }
2581
+ }
2582
+ /**
2583
+ * Cancel active sync for a space
2584
+ */
2585
+ cancelSync(spaceId) {
2586
+ const session = this.sessions.get(spaceId);
2587
+ if (session?.timeout) {
2588
+ clearTimeout(session.timeout);
2589
+ }
2590
+ this.sessions.delete(spaceId);
2591
+ }
2592
+ };
2593
+ export {
2594
+ AGGRESSIVE_SYNC_TIMEOUT_MS,
2595
+ ApiError,
2596
+ ApiErrorCode,
2597
+ BOOKMARKS_CONFIG,
2598
+ BrowserWebSocketClient,
2599
+ DEFAULT_SYNC_EXPIRY_MS,
2600
+ ENCRYPTION_STORAGE_KEYS,
2601
+ MAX_CHUNK_SIZE,
2602
+ MAX_MENTIONS,
2603
+ MAX_MESSAGE_LENGTH,
2604
+ MENTION_PATTERNS,
2605
+ RNWebSocketClient,
2606
+ SyncService,
2607
+ WasmCryptoProvider,
2608
+ WasmSigningProvider,
2609
+ base64ToBytes,
2610
+ buildMemberDelta,
2611
+ buildMessageDelta,
2612
+ buildReactionDelta,
2613
+ bytesToBase64,
2614
+ bytesToHex,
2615
+ bytesToString,
2616
+ chunkMembers,
2617
+ chunkMessages,
2618
+ computeContentHash,
2619
+ computeHash,
2620
+ computeManifestHash,
2621
+ computeMemberDiff,
2622
+ computeMemberHash,
2623
+ computeMessageDiff,
2624
+ computePeerDiff,
2625
+ computeReactionDiff,
2626
+ computeReactionHash,
2627
+ createApiError,
2628
+ createBrowserWebSocketClient,
2629
+ createManifest,
2630
+ createMemberDigest,
2631
+ createMessageDigest,
2632
+ createNetworkError,
2633
+ createRNWebSocketClient,
2634
+ createReactionDigest,
2635
+ createSignedMessage,
2636
+ createSyncSummary,
2637
+ endpoints,
2638
+ extractMentions,
2639
+ findChannel,
2640
+ flattenChannels,
2641
+ flattenMessages,
2642
+ formatDate,
2643
+ formatDateTime,
2644
+ formatFileSize,
2645
+ formatMemberCount,
2646
+ formatMention,
2647
+ formatMessageDate,
2648
+ formatRelativeTime,
2649
+ formatTime,
2650
+ hexToBytes,
2651
+ int64ToBytes,
2652
+ isSameDay,
2653
+ isSyncDelta,
2654
+ isSyncInfo,
2655
+ isSyncInitiate,
2656
+ isSyncManifest,
2657
+ isSyncRequest,
2658
+ logger,
2659
+ parseMentions,
2660
+ queryKeys,
2661
+ sanitizeContent,
2662
+ stringToBytes,
2663
+ truncateText,
2664
+ useAddReaction,
2665
+ useChannels,
2666
+ useDeleteMessage,
2667
+ useEditMessage,
2668
+ useInvalidateMessages,
2669
+ useMessages,
2670
+ useRemoveReaction,
2671
+ useSendMessage,
2672
+ useSpace,
2673
+ useSpaceMembers,
2674
+ useSpaces,
2675
+ validateMessage,
2676
+ validateMessageContent,
2677
+ verifySignedMessage
2678
+ };