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