@quilibrium/quorum-shared 2.1.0-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +2414 -0
- package/dist/index.d.ts +2414 -0
- package/dist/index.js +2788 -0
- package/dist/index.mjs +2678 -0
- package/package.json +49 -0
- package/src/api/client.ts +86 -0
- package/src/api/endpoints.ts +87 -0
- package/src/api/errors.ts +179 -0
- package/src/api/index.ts +35 -0
- package/src/crypto/encryption-state.ts +249 -0
- package/src/crypto/index.ts +55 -0
- package/src/crypto/types.ts +307 -0
- package/src/crypto/wasm-provider.ts +298 -0
- package/src/hooks/index.ts +31 -0
- package/src/hooks/keys.ts +62 -0
- package/src/hooks/mutations/index.ts +15 -0
- package/src/hooks/mutations/useDeleteMessage.ts +67 -0
- package/src/hooks/mutations/useEditMessage.ts +87 -0
- package/src/hooks/mutations/useReaction.ts +163 -0
- package/src/hooks/mutations/useSendMessage.ts +131 -0
- package/src/hooks/useChannels.ts +49 -0
- package/src/hooks/useMessages.ts +77 -0
- package/src/hooks/useSpaces.ts +60 -0
- package/src/index.ts +32 -0
- package/src/signing/index.ts +10 -0
- package/src/signing/types.ts +83 -0
- package/src/signing/wasm-provider.ts +75 -0
- package/src/storage/adapter.ts +118 -0
- package/src/storage/index.ts +9 -0
- package/src/sync/index.ts +83 -0
- package/src/sync/service.test.ts +822 -0
- package/src/sync/service.ts +947 -0
- package/src/sync/types.ts +267 -0
- package/src/sync/utils.ts +588 -0
- package/src/transport/browser-websocket.ts +299 -0
- package/src/transport/index.ts +34 -0
- package/src/transport/rn-websocket.ts +321 -0
- package/src/transport/types.ts +56 -0
- package/src/transport/websocket.ts +212 -0
- package/src/types/bookmark.ts +29 -0
- package/src/types/conversation.ts +25 -0
- package/src/types/index.ts +57 -0
- package/src/types/message.ts +178 -0
- package/src/types/space.ts +75 -0
- package/src/types/user.ts +72 -0
- package/src/utils/encoding.ts +106 -0
- package/src/utils/formatting.ts +139 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/logger.ts +141 -0
- package/src/utils/mentions.ts +135 -0
- package/src/utils/validation.ts +84 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser/Electron WebSocket client implementation
|
|
3
|
+
*
|
|
4
|
+
* Uses the native WebSocket API available in browsers and Electron.
|
|
5
|
+
* Implements automatic reconnection, dual queue system, and subscription management.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
WebSocketClient,
|
|
10
|
+
WebSocketClientOptions,
|
|
11
|
+
WebSocketConnectionState,
|
|
12
|
+
EncryptedWebSocketMessage,
|
|
13
|
+
MessageHandler,
|
|
14
|
+
StateChangeHandler,
|
|
15
|
+
ErrorHandler,
|
|
16
|
+
} from './websocket';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* BrowserWebSocketClient - WebSocket implementation for browser/Electron
|
|
20
|
+
*
|
|
21
|
+
* Features:
|
|
22
|
+
* - Automatic reconnection with configurable interval
|
|
23
|
+
* - Dual queue system (inbound messages, outbound message generators)
|
|
24
|
+
* - Inbox address subscription management
|
|
25
|
+
* - Periodic queue processing
|
|
26
|
+
*/
|
|
27
|
+
export class BrowserWebSocketClient implements WebSocketClient {
|
|
28
|
+
private url: string;
|
|
29
|
+
private reconnectInterval: number;
|
|
30
|
+
private maxReconnectAttempts: number;
|
|
31
|
+
private queueProcessInterval: number;
|
|
32
|
+
|
|
33
|
+
private ws: WebSocket | null = null;
|
|
34
|
+
private _state: WebSocketConnectionState = 'disconnected';
|
|
35
|
+
private reconnectAttempts = 0;
|
|
36
|
+
private shouldReconnect = true;
|
|
37
|
+
|
|
38
|
+
// Queue system
|
|
39
|
+
private inboundQueue: EncryptedWebSocketMessage[] = [];
|
|
40
|
+
private outboundQueue: Array<() => Promise<string[]>> = [];
|
|
41
|
+
private isProcessing = false;
|
|
42
|
+
private processIntervalId: ReturnType<typeof setInterval> | null = null;
|
|
43
|
+
|
|
44
|
+
// Handlers
|
|
45
|
+
private messageHandler: MessageHandler | null = null;
|
|
46
|
+
private resubscribeHandler: (() => Promise<void>) | null = null;
|
|
47
|
+
private stateChangeHandlers: Set<StateChangeHandler> = new Set();
|
|
48
|
+
private errorHandlers: Set<ErrorHandler> = new Set();
|
|
49
|
+
|
|
50
|
+
constructor(options: WebSocketClientOptions) {
|
|
51
|
+
this.url = options.url;
|
|
52
|
+
this.reconnectInterval = options.reconnectInterval ?? 1000;
|
|
53
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
|
|
54
|
+
this.queueProcessInterval = options.queueProcessInterval ?? 1000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get state(): WebSocketConnectionState {
|
|
58
|
+
return this._state;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get isConnected(): boolean {
|
|
62
|
+
return this._state === 'connected';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private setState(newState: WebSocketConnectionState): void {
|
|
66
|
+
if (this._state !== newState) {
|
|
67
|
+
this._state = newState;
|
|
68
|
+
this.stateChangeHandlers.forEach((handler) => {
|
|
69
|
+
try {
|
|
70
|
+
handler(newState);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('Error in state change handler:', error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private emitError(error: Error): void {
|
|
79
|
+
this.errorHandlers.forEach((handler) => {
|
|
80
|
+
try {
|
|
81
|
+
handler(error);
|
|
82
|
+
} catch (e) {
|
|
83
|
+
console.error('Error in error handler:', e);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async connect(): Promise<void> {
|
|
89
|
+
if (this._state === 'connected' || this._state === 'connecting') {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.shouldReconnect = true;
|
|
94
|
+
return this.doConnect();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private doConnect(): Promise<void> {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
this.setState('connecting');
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
this.ws = new WebSocket(this.url);
|
|
103
|
+
|
|
104
|
+
this.ws.onopen = () => {
|
|
105
|
+
this.reconnectAttempts = 0;
|
|
106
|
+
this.setState('connected');
|
|
107
|
+
this.startQueueProcessing();
|
|
108
|
+
|
|
109
|
+
// Call resubscribe handler to restore subscriptions
|
|
110
|
+
if (this.resubscribeHandler) {
|
|
111
|
+
this.resubscribeHandler().catch((error) => {
|
|
112
|
+
console.error('Error in resubscribe handler:', error);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Process any pending outbound messages
|
|
117
|
+
this.processQueues();
|
|
118
|
+
resolve();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
this.ws.onclose = () => {
|
|
122
|
+
this.setState('disconnected');
|
|
123
|
+
this.stopQueueProcessing();
|
|
124
|
+
|
|
125
|
+
if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
126
|
+
this.reconnectAttempts++;
|
|
127
|
+
this.setState('reconnecting');
|
|
128
|
+
setTimeout(() => this.doConnect(), this.reconnectInterval);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
this.ws.onerror = (event) => {
|
|
133
|
+
const error = new Error('WebSocket error');
|
|
134
|
+
this.emitError(error);
|
|
135
|
+
reject(error);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
this.ws.onmessage = (event) => {
|
|
139
|
+
try {
|
|
140
|
+
const message = JSON.parse(event.data) as EncryptedWebSocketMessage;
|
|
141
|
+
this.inboundQueue.push(message);
|
|
142
|
+
this.processQueues();
|
|
143
|
+
} catch (error) {
|
|
144
|
+
console.error('Failed to parse WebSocket message:', error);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.setState('disconnected');
|
|
149
|
+
reject(error);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
disconnect(): void {
|
|
155
|
+
this.shouldReconnect = false;
|
|
156
|
+
this.stopQueueProcessing();
|
|
157
|
+
|
|
158
|
+
if (this.ws) {
|
|
159
|
+
this.ws.close();
|
|
160
|
+
this.ws = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.setState('disconnected');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async send(message: string): Promise<void> {
|
|
167
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
168
|
+
this.ws.send(message);
|
|
169
|
+
} else {
|
|
170
|
+
// Queue the message for later
|
|
171
|
+
this.outboundQueue.push(async () => [message]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
enqueueOutbound(prepareMessage: () => Promise<string[]>): void {
|
|
176
|
+
this.outboundQueue.push(prepareMessage);
|
|
177
|
+
this.processQueues();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async subscribe(inboxAddresses: string[]): Promise<void> {
|
|
181
|
+
const message = JSON.stringify({
|
|
182
|
+
type: 'listen',
|
|
183
|
+
inbox_addresses: inboxAddresses,
|
|
184
|
+
});
|
|
185
|
+
await this.send(message);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async unsubscribe(inboxAddresses: string[]): Promise<void> {
|
|
189
|
+
const message = JSON.stringify({
|
|
190
|
+
type: 'unlisten',
|
|
191
|
+
inbox_addresses: inboxAddresses,
|
|
192
|
+
});
|
|
193
|
+
await this.send(message);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
setMessageHandler(handler: MessageHandler): void {
|
|
197
|
+
this.messageHandler = handler;
|
|
198
|
+
this.processQueues();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
setResubscribeHandler(handler: () => Promise<void>): void {
|
|
202
|
+
this.resubscribeHandler = handler;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
onStateChange(handler: StateChangeHandler): () => void {
|
|
206
|
+
this.stateChangeHandlers.add(handler);
|
|
207
|
+
return () => {
|
|
208
|
+
this.stateChangeHandlers.delete(handler);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
onError(handler: ErrorHandler): () => void {
|
|
213
|
+
this.errorHandlers.add(handler);
|
|
214
|
+
return () => {
|
|
215
|
+
this.errorHandlers.delete(handler);
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private startQueueProcessing(): void {
|
|
220
|
+
if (this.processIntervalId === null) {
|
|
221
|
+
this.processIntervalId = setInterval(() => {
|
|
222
|
+
this.processQueues();
|
|
223
|
+
}, this.queueProcessInterval);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private stopQueueProcessing(): void {
|
|
228
|
+
if (this.processIntervalId !== null) {
|
|
229
|
+
clearInterval(this.processIntervalId);
|
|
230
|
+
this.processIntervalId = null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private async processQueues(): Promise<void> {
|
|
235
|
+
if (this.isProcessing) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
this.isProcessing = true;
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
// Process inbound messages
|
|
243
|
+
if (this.messageHandler) {
|
|
244
|
+
// Group messages by inbox address for efficient processing
|
|
245
|
+
const inboxMap = new Map<string, EncryptedWebSocketMessage[]>();
|
|
246
|
+
|
|
247
|
+
while (this.inboundQueue.length > 0) {
|
|
248
|
+
const message = this.inboundQueue.shift()!;
|
|
249
|
+
const existing = inboxMap.get(message.inboxAddress) || [];
|
|
250
|
+
existing.push(message);
|
|
251
|
+
inboxMap.set(message.inboxAddress, existing);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Process all inbox groups concurrently
|
|
255
|
+
const promises: Promise<void>[] = [];
|
|
256
|
+
|
|
257
|
+
for (const [_, messages] of inboxMap) {
|
|
258
|
+
promises.push(
|
|
259
|
+
(async () => {
|
|
260
|
+
for (const message of messages) {
|
|
261
|
+
try {
|
|
262
|
+
await this.messageHandler!(message);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('Error processing inbound message:', error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
})()
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
await Promise.allSettled(promises);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Process outbound messages only if connected
|
|
275
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
276
|
+
while (this.outboundQueue.length > 0) {
|
|
277
|
+
const prepareMessage = this.outboundQueue.shift()!;
|
|
278
|
+
try {
|
|
279
|
+
const messages = await prepareMessage();
|
|
280
|
+
for (const m of messages) {
|
|
281
|
+
this.ws.send(m);
|
|
282
|
+
}
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.error('Error processing outbound message:', error);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
this.isProcessing = false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Factory function to create a browser WebSocket client
|
|
296
|
+
*/
|
|
297
|
+
export function createBrowserWebSocketClient(options: WebSocketClientOptions): WebSocketClient {
|
|
298
|
+
return new BrowserWebSocketClient(options);
|
|
299
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport module exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// HTTP transport types
|
|
6
|
+
export type {
|
|
7
|
+
TransportConfig,
|
|
8
|
+
TransportRequestOptions,
|
|
9
|
+
TransportResponse,
|
|
10
|
+
TransportClient,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
// WebSocket types
|
|
14
|
+
export type {
|
|
15
|
+
WebSocketConnectionState,
|
|
16
|
+
EncryptedWebSocketMessage,
|
|
17
|
+
SealedMessage,
|
|
18
|
+
UnsealedEnvelope,
|
|
19
|
+
OutboundWebSocketMessage,
|
|
20
|
+
ListenMessage,
|
|
21
|
+
UnlistenMessage,
|
|
22
|
+
WebSocketClientOptions,
|
|
23
|
+
MessageHandler,
|
|
24
|
+
StateChangeHandler,
|
|
25
|
+
ErrorHandler,
|
|
26
|
+
WebSocketClient,
|
|
27
|
+
CreateWebSocketClient,
|
|
28
|
+
} from './websocket';
|
|
29
|
+
|
|
30
|
+
// Browser/Electron WebSocket implementation
|
|
31
|
+
export { BrowserWebSocketClient, createBrowserWebSocketClient } from './browser-websocket';
|
|
32
|
+
|
|
33
|
+
// React Native WebSocket implementation
|
|
34
|
+
export { RNWebSocketClient, createRNWebSocketClient } from './rn-websocket';
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native WebSocket client implementation
|
|
3
|
+
*
|
|
4
|
+
* Uses the React Native WebSocket API which is similar to the browser's.
|
|
5
|
+
* Implements automatic reconnection, dual queue system, and subscription management.
|
|
6
|
+
*
|
|
7
|
+
* Note: This implementation is nearly identical to browser-websocket.ts
|
|
8
|
+
* but kept separate to allow for React Native-specific optimizations if needed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
WebSocketClient,
|
|
13
|
+
WebSocketClientOptions,
|
|
14
|
+
WebSocketConnectionState,
|
|
15
|
+
EncryptedWebSocketMessage,
|
|
16
|
+
MessageHandler,
|
|
17
|
+
StateChangeHandler,
|
|
18
|
+
ErrorHandler,
|
|
19
|
+
} from './websocket';
|
|
20
|
+
import { logger } from '../utils/logger';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* RNWebSocketClient - WebSocket implementation for React Native
|
|
24
|
+
*
|
|
25
|
+
* Features:
|
|
26
|
+
* - Automatic reconnection with configurable interval
|
|
27
|
+
* - Dual queue system (inbound messages, outbound message generators)
|
|
28
|
+
* - Inbox address subscription management
|
|
29
|
+
* - Periodic queue processing
|
|
30
|
+
*/
|
|
31
|
+
export class RNWebSocketClient implements WebSocketClient {
|
|
32
|
+
private url: string;
|
|
33
|
+
private reconnectInterval: number;
|
|
34
|
+
private maxReconnectAttempts: number;
|
|
35
|
+
private queueProcessInterval: number;
|
|
36
|
+
|
|
37
|
+
private ws: WebSocket | null = null;
|
|
38
|
+
private _state: WebSocketConnectionState = 'disconnected';
|
|
39
|
+
private reconnectAttempts = 0;
|
|
40
|
+
private shouldReconnect = true;
|
|
41
|
+
|
|
42
|
+
// Queue system
|
|
43
|
+
private inboundQueue: EncryptedWebSocketMessage[] = [];
|
|
44
|
+
private outboundQueue: Array<() => Promise<string[]>> = [];
|
|
45
|
+
private isProcessing = false;
|
|
46
|
+
private processIntervalId: ReturnType<typeof setInterval> | null = null;
|
|
47
|
+
|
|
48
|
+
// Handlers
|
|
49
|
+
private messageHandler: MessageHandler | null = null;
|
|
50
|
+
private resubscribeHandler: (() => Promise<void>) | null = null;
|
|
51
|
+
private stateChangeHandlers: Set<StateChangeHandler> = new Set();
|
|
52
|
+
private errorHandlers: Set<ErrorHandler> = new Set();
|
|
53
|
+
|
|
54
|
+
constructor(options: WebSocketClientOptions) {
|
|
55
|
+
this.url = options.url;
|
|
56
|
+
this.reconnectInterval = options.reconnectInterval ?? 1000;
|
|
57
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? Infinity;
|
|
58
|
+
this.queueProcessInterval = options.queueProcessInterval ?? 1000;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get state(): WebSocketConnectionState {
|
|
62
|
+
return this._state;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get isConnected(): boolean {
|
|
66
|
+
return this._state === 'connected';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private setState(newState: WebSocketConnectionState): void {
|
|
70
|
+
if (this._state !== newState) {
|
|
71
|
+
this._state = newState;
|
|
72
|
+
this.stateChangeHandlers.forEach((handler) => {
|
|
73
|
+
try {
|
|
74
|
+
handler(newState);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('Error in state change handler:', error);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private emitError(error: Error): void {
|
|
83
|
+
this.errorHandlers.forEach((handler) => {
|
|
84
|
+
try {
|
|
85
|
+
handler(error);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.error('Error in error handler:', e);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async connect(): Promise<void> {
|
|
93
|
+
if (this._state === 'connected' || this._state === 'connecting') {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.shouldReconnect = true;
|
|
98
|
+
return this.doConnect();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private doConnect(): Promise<void> {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
this.setState('connecting');
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
// React Native uses the global WebSocket class
|
|
107
|
+
this.ws = new WebSocket(this.url);
|
|
108
|
+
|
|
109
|
+
this.ws.onopen = () => {
|
|
110
|
+
this.reconnectAttempts = 0;
|
|
111
|
+
this.setState('connected');
|
|
112
|
+
this.startQueueProcessing();
|
|
113
|
+
|
|
114
|
+
// Call resubscribe handler to restore subscriptions
|
|
115
|
+
if (this.resubscribeHandler) {
|
|
116
|
+
this.resubscribeHandler().catch((error) => {
|
|
117
|
+
console.error('Error in resubscribe handler:', error);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Process any pending outbound messages
|
|
122
|
+
this.processQueues();
|
|
123
|
+
resolve();
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
this.ws.onclose = () => {
|
|
127
|
+
this.setState('disconnected');
|
|
128
|
+
this.stopQueueProcessing();
|
|
129
|
+
|
|
130
|
+
if (this.shouldReconnect && this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
131
|
+
this.reconnectAttempts++;
|
|
132
|
+
this.setState('reconnecting');
|
|
133
|
+
setTimeout(() => this.doConnect(), this.reconnectInterval);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
this.ws.onerror = () => {
|
|
138
|
+
const error = new Error('WebSocket error');
|
|
139
|
+
this.emitError(error);
|
|
140
|
+
reject(error);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
this.ws.onmessage = (event: { data: string }) => {
|
|
144
|
+
try {
|
|
145
|
+
// Log raw message for debugging
|
|
146
|
+
logger.debug('[WS-RN] Raw message received:', event.data?.substring(0, 200));
|
|
147
|
+
|
|
148
|
+
// Some server messages might not be encrypted content (e.g., acks, pings)
|
|
149
|
+
// Skip messages that don't look like JSON objects
|
|
150
|
+
if (!event.data || !event.data.startsWith('{')) {
|
|
151
|
+
logger.debug('[WS-RN] Ignoring non-JSON message');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const message = JSON.parse(event.data) as EncryptedWebSocketMessage;
|
|
156
|
+
|
|
157
|
+
// Only process messages that have the expected structure
|
|
158
|
+
if (!message.inboxAddress && !message.encryptedContent) {
|
|
159
|
+
logger.debug('[WS-RN] Ignoring message without expected fields:', Object.keys(message));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.inboundQueue.push(message);
|
|
164
|
+
this.processQueues();
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('Failed to parse WebSocket message:', error, 'raw:', event.data?.substring(0, 100));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
this.setState('disconnected');
|
|
171
|
+
reject(error);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
disconnect(): void {
|
|
177
|
+
this.shouldReconnect = false;
|
|
178
|
+
this.stopQueueProcessing();
|
|
179
|
+
|
|
180
|
+
if (this.ws) {
|
|
181
|
+
this.ws.close();
|
|
182
|
+
this.ws = null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.setState('disconnected');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async send(message: string): Promise<void> {
|
|
189
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
190
|
+
this.ws.send(message);
|
|
191
|
+
} else {
|
|
192
|
+
// Queue the message for later
|
|
193
|
+
this.outboundQueue.push(async () => [message]);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
enqueueOutbound(prepareMessage: () => Promise<string[]>): void {
|
|
198
|
+
this.outboundQueue.push(prepareMessage);
|
|
199
|
+
this.processQueues();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async subscribe(inboxAddresses: string[]): Promise<void> {
|
|
203
|
+
const message = JSON.stringify({
|
|
204
|
+
type: 'listen',
|
|
205
|
+
inbox_addresses: inboxAddresses,
|
|
206
|
+
});
|
|
207
|
+
await this.send(message);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async unsubscribe(inboxAddresses: string[]): Promise<void> {
|
|
211
|
+
const message = JSON.stringify({
|
|
212
|
+
type: 'unlisten',
|
|
213
|
+
inbox_addresses: inboxAddresses,
|
|
214
|
+
});
|
|
215
|
+
await this.send(message);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
setMessageHandler(handler: MessageHandler): void {
|
|
219
|
+
this.messageHandler = handler;
|
|
220
|
+
this.processQueues();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
setResubscribeHandler(handler: () => Promise<void>): void {
|
|
224
|
+
this.resubscribeHandler = handler;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
onStateChange(handler: StateChangeHandler): () => void {
|
|
228
|
+
this.stateChangeHandlers.add(handler);
|
|
229
|
+
return () => {
|
|
230
|
+
this.stateChangeHandlers.delete(handler);
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
onError(handler: ErrorHandler): () => void {
|
|
235
|
+
this.errorHandlers.add(handler);
|
|
236
|
+
return () => {
|
|
237
|
+
this.errorHandlers.delete(handler);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private startQueueProcessing(): void {
|
|
242
|
+
if (this.processIntervalId === null) {
|
|
243
|
+
this.processIntervalId = setInterval(() => {
|
|
244
|
+
this.processQueues();
|
|
245
|
+
}, this.queueProcessInterval);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private stopQueueProcessing(): void {
|
|
250
|
+
if (this.processIntervalId !== null) {
|
|
251
|
+
clearInterval(this.processIntervalId);
|
|
252
|
+
this.processIntervalId = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async processQueues(): Promise<void> {
|
|
257
|
+
if (this.isProcessing) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.isProcessing = true;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// Process inbound messages
|
|
265
|
+
if (this.messageHandler) {
|
|
266
|
+
// Group messages by inbox address for efficient processing
|
|
267
|
+
const inboxMap = new Map<string, EncryptedWebSocketMessage[]>();
|
|
268
|
+
|
|
269
|
+
while (this.inboundQueue.length > 0) {
|
|
270
|
+
const message = this.inboundQueue.shift()!;
|
|
271
|
+
const existing = inboxMap.get(message.inboxAddress) || [];
|
|
272
|
+
existing.push(message);
|
|
273
|
+
inboxMap.set(message.inboxAddress, existing);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Process all inbox groups concurrently
|
|
277
|
+
const promises: Promise<void>[] = [];
|
|
278
|
+
|
|
279
|
+
for (const [_, messages] of inboxMap) {
|
|
280
|
+
promises.push(
|
|
281
|
+
(async () => {
|
|
282
|
+
for (const message of messages) {
|
|
283
|
+
try {
|
|
284
|
+
await this.messageHandler!(message);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
console.error('Error processing inbound message:', error);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
})()
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
await Promise.allSettled(promises);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Process outbound messages only if connected
|
|
297
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
298
|
+
while (this.outboundQueue.length > 0) {
|
|
299
|
+
const prepareMessage = this.outboundQueue.shift()!;
|
|
300
|
+
try {
|
|
301
|
+
const messages = await prepareMessage();
|
|
302
|
+
for (const m of messages) {
|
|
303
|
+
this.ws.send(m);
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error('Error processing outbound message:', error);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
} finally {
|
|
311
|
+
this.isProcessing = false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Factory function to create a React Native WebSocket client
|
|
318
|
+
*/
|
|
319
|
+
export function createRNWebSocketClient(options: WebSocketClientOptions): WebSocketClient {
|
|
320
|
+
return new RNWebSocketClient(options);
|
|
321
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport types and interfaces
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic transport layer for HTTP and IPC communication.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============ HTTP Transport ============
|
|
8
|
+
|
|
9
|
+
export interface TransportConfig {
|
|
10
|
+
baseUrl: string;
|
|
11
|
+
timeout?: number;
|
|
12
|
+
defaultHeaders?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TransportRequestOptions {
|
|
16
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
body?: unknown;
|
|
19
|
+
timeout?: number;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TransportResponse<T> {
|
|
24
|
+
data: T;
|
|
25
|
+
status: number;
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* TransportClient - Platform-agnostic HTTP transport
|
|
31
|
+
*
|
|
32
|
+
* Implementations:
|
|
33
|
+
* - Fetch (mobile/web): Uses native fetch API
|
|
34
|
+
* - Electron IPC (desktop): Uses Electron IPC for main process requests
|
|
35
|
+
*/
|
|
36
|
+
export interface TransportClient {
|
|
37
|
+
/**
|
|
38
|
+
* Make an HTTP request
|
|
39
|
+
*/
|
|
40
|
+
request<T>(endpoint: string, options?: TransportRequestOptions): Promise<TransportResponse<T>>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Configure the transport client
|
|
44
|
+
*/
|
|
45
|
+
configure(config: TransportConfig): void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set authorization header for all requests
|
|
49
|
+
*/
|
|
50
|
+
setAuthToken(token: string): void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Clear authorization
|
|
54
|
+
*/
|
|
55
|
+
clearAuth(): void;
|
|
56
|
+
}
|