@quilibrium/quorum-shared 2.1.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +2414 -0
- package/dist/index.d.ts +2414 -0
- package/dist/index.js +2788 -0
- package/dist/index.mjs +2678 -0
- package/package.json +49 -0
- package/src/api/client.ts +86 -0
- package/src/api/endpoints.ts +87 -0
- package/src/api/errors.ts +179 -0
- package/src/api/index.ts +35 -0
- package/src/crypto/encryption-state.ts +249 -0
- package/src/crypto/index.ts +55 -0
- package/src/crypto/types.ts +307 -0
- package/src/crypto/wasm-provider.ts +298 -0
- package/src/hooks/index.ts +31 -0
- package/src/hooks/keys.ts +62 -0
- package/src/hooks/mutations/index.ts +15 -0
- package/src/hooks/mutations/useDeleteMessage.ts +67 -0
- package/src/hooks/mutations/useEditMessage.ts +87 -0
- package/src/hooks/mutations/useReaction.ts +163 -0
- package/src/hooks/mutations/useSendMessage.ts +131 -0
- package/src/hooks/useChannels.ts +49 -0
- package/src/hooks/useMessages.ts +77 -0
- package/src/hooks/useSpaces.ts +60 -0
- package/src/index.ts +32 -0
- package/src/signing/index.ts +10 -0
- package/src/signing/types.ts +83 -0
- package/src/signing/wasm-provider.ts +75 -0
- package/src/storage/adapter.ts +118 -0
- package/src/storage/index.ts +9 -0
- package/src/sync/index.ts +83 -0
- package/src/sync/service.test.ts +822 -0
- package/src/sync/service.ts +947 -0
- package/src/sync/types.ts +267 -0
- package/src/sync/utils.ts +588 -0
- package/src/transport/browser-websocket.ts +299 -0
- package/src/transport/index.ts +34 -0
- package/src/transport/rn-websocket.ts +321 -0
- package/src/transport/types.ts +56 -0
- package/src/transport/websocket.ts +212 -0
- package/src/types/bookmark.ts +29 -0
- package/src/types/conversation.ts +25 -0
- package/src/types/index.ts +57 -0
- package/src/types/message.ts +178 -0
- package/src/types/space.ts +75 -0
- package/src/types/user.ts +72 -0
- package/src/utils/encoding.ts +106 -0
- package/src/utils/formatting.ts +139 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/mentions.ts +135 -0
- package/src/utils/validation.ts +84 -0
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
|
+
});
|