@pedi/chika-sdk 1.0.5 → 1.0.7
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/README.md +54 -1
- package/dist/index.d.mts +99 -11
- package/dist/index.d.ts +99 -11
- package/dist/index.js +764 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +760 -71
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -3
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/use-chat.ts
|
|
2
9
|
import { useEffect, useRef, useState, useCallback } from "react";
|
|
3
10
|
import { AppState, Platform } from "react-native";
|
|
@@ -17,6 +24,104 @@ var ChannelClosedError = class extends Error {
|
|
|
17
24
|
this.name = "ChannelClosedError";
|
|
18
25
|
}
|
|
19
26
|
};
|
|
27
|
+
var HttpError = class extends Error {
|
|
28
|
+
constructor(status, body, retryAfter) {
|
|
29
|
+
super(`HTTP ${status}: ${body}`);
|
|
30
|
+
this.status = status;
|
|
31
|
+
this.body = body;
|
|
32
|
+
this.retryAfter = retryAfter;
|
|
33
|
+
this.name = "HttpError";
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var RetryExhaustedError = class extends Error {
|
|
37
|
+
constructor(operation, attempts, lastError) {
|
|
38
|
+
super(`${operation} failed after ${attempts} attempts: ${lastError.message}`);
|
|
39
|
+
this.operation = operation;
|
|
40
|
+
this.attempts = attempts;
|
|
41
|
+
this.lastError = lastError;
|
|
42
|
+
this.name = "RetryExhaustedError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var QueueFullError = class extends Error {
|
|
46
|
+
constructor(maxSize) {
|
|
47
|
+
super(`Message queue full (max ${maxSize})`);
|
|
48
|
+
this.maxSize = maxSize;
|
|
49
|
+
this.name = "QueueFullError";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/retry.ts
|
|
54
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
55
|
+
maxAttempts: 3,
|
|
56
|
+
baseDelayMs: 1e3,
|
|
57
|
+
maxDelayMs: 1e4,
|
|
58
|
+
jitterFactor: 0.3
|
|
59
|
+
};
|
|
60
|
+
function calculateBackoff(attempt, config) {
|
|
61
|
+
const delay = Math.min(config.baseDelayMs * 2 ** attempt, config.maxDelayMs);
|
|
62
|
+
const jitter = 1 + (Math.random() * 2 - 1) * config.jitterFactor;
|
|
63
|
+
return Math.round(delay * jitter);
|
|
64
|
+
}
|
|
65
|
+
function isRetryableError(error) {
|
|
66
|
+
if (error instanceof ChannelClosedError) return false;
|
|
67
|
+
if (error instanceof ChatDisconnectedError) return false;
|
|
68
|
+
if (error instanceof QueueFullError) return false;
|
|
69
|
+
if (error instanceof DOMException && error.name === "AbortError") return false;
|
|
70
|
+
if (error instanceof HttpError) {
|
|
71
|
+
const { status } = error;
|
|
72
|
+
if (status === 408 || status === 429) return true;
|
|
73
|
+
if (status >= 500) return true;
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
if (error instanceof TypeError) return true;
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
function sleep(ms, signal) {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
if (signal?.aborted) {
|
|
82
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const timer = setTimeout(resolve, ms);
|
|
86
|
+
signal?.addEventListener(
|
|
87
|
+
"abort",
|
|
88
|
+
() => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
91
|
+
},
|
|
92
|
+
{ once: true }
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function resolveRetryConfig(resilience) {
|
|
97
|
+
if (resilience === false) return null;
|
|
98
|
+
if (!resilience || resilience.retry === void 0) return DEFAULT_RETRY_CONFIG;
|
|
99
|
+
if (resilience.retry === false) return null;
|
|
100
|
+
return { ...DEFAULT_RETRY_CONFIG, ...resilience.retry };
|
|
101
|
+
}
|
|
102
|
+
async function withRetry(fn, config = DEFAULT_RETRY_CONFIG, signal) {
|
|
103
|
+
let lastError;
|
|
104
|
+
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
|
105
|
+
if (signal?.aborted) {
|
|
106
|
+
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
return await fn();
|
|
110
|
+
} catch (err) {
|
|
111
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
112
|
+
if (!isRetryableError(err)) throw lastError;
|
|
113
|
+
if (attempt < config.maxAttempts - 1) {
|
|
114
|
+
const delayMs = err instanceof HttpError && err.retryAfter != null ? err.retryAfter * 1e3 : calculateBackoff(attempt, config);
|
|
115
|
+
await sleep(delayMs, signal);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw new RetryExhaustedError(
|
|
120
|
+
"operation",
|
|
121
|
+
config.maxAttempts,
|
|
122
|
+
lastError
|
|
123
|
+
);
|
|
124
|
+
}
|
|
20
125
|
|
|
21
126
|
// src/resolve-url.ts
|
|
22
127
|
function createManifest(serverUrl) {
|
|
@@ -35,27 +140,55 @@ function resolveServerUrl(manifest, channelId) {
|
|
|
35
140
|
import EventSource from "react-native-sse";
|
|
36
141
|
var DEFAULT_RECONNECT_DELAY_MS = 3e3;
|
|
37
142
|
function createSSEConnection(config, callbacks) {
|
|
38
|
-
const
|
|
143
|
+
const baseDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;
|
|
39
144
|
const customEvents = config.customEvents ?? [];
|
|
145
|
+
const monitor = config.networkMonitor;
|
|
146
|
+
const backoffConfig = {
|
|
147
|
+
maxAttempts: Infinity,
|
|
148
|
+
baseDelayMs: baseDelay,
|
|
149
|
+
maxDelayMs: 3e4,
|
|
150
|
+
jitterFactor: 0.3
|
|
151
|
+
};
|
|
40
152
|
let currentLastEventId = config.lastEventId;
|
|
41
153
|
let es = null;
|
|
42
154
|
let disposed = false;
|
|
43
155
|
let reconnectTimer = null;
|
|
156
|
+
let attempt = 0;
|
|
157
|
+
let waitAbort = null;
|
|
44
158
|
function cleanup() {
|
|
45
159
|
if (es) {
|
|
46
160
|
es.removeAllEventListeners();
|
|
47
161
|
es.close();
|
|
48
162
|
es = null;
|
|
49
163
|
}
|
|
164
|
+
if (reconnectTimer) {
|
|
165
|
+
clearTimeout(reconnectTimer);
|
|
166
|
+
reconnectTimer = null;
|
|
167
|
+
}
|
|
168
|
+
if (waitAbort) {
|
|
169
|
+
waitAbort.abort();
|
|
170
|
+
waitAbort = null;
|
|
171
|
+
}
|
|
50
172
|
}
|
|
51
|
-
function scheduleReconnect() {
|
|
52
|
-
if (disposed || reconnectTimer) return;
|
|
173
|
+
async function scheduleReconnect() {
|
|
174
|
+
if (disposed || reconnectTimer || waitAbort) return;
|
|
53
175
|
callbacks.onReconnecting?.();
|
|
54
176
|
cleanup();
|
|
177
|
+
if (monitor && !monitor.isConnected()) {
|
|
178
|
+
waitAbort = new AbortController();
|
|
179
|
+
try {
|
|
180
|
+
await monitor.waitForOnline(waitAbort.signal);
|
|
181
|
+
} catch {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
waitAbort = null;
|
|
185
|
+
if (disposed) return;
|
|
186
|
+
}
|
|
187
|
+
const delay = calculateBackoff(attempt++, backoffConfig);
|
|
55
188
|
reconnectTimer = setTimeout(() => {
|
|
56
189
|
reconnectTimer = null;
|
|
57
190
|
connect();
|
|
58
|
-
},
|
|
191
|
+
}, delay);
|
|
59
192
|
}
|
|
60
193
|
function connect() {
|
|
61
194
|
if (disposed) return;
|
|
@@ -68,6 +201,7 @@ function createSSEConnection(config, callbacks) {
|
|
|
68
201
|
});
|
|
69
202
|
es.addEventListener("open", () => {
|
|
70
203
|
if (disposed) return;
|
|
204
|
+
attempt = 0;
|
|
71
205
|
callbacks.onOpen?.();
|
|
72
206
|
});
|
|
73
207
|
es.addEventListener("message", (event) => {
|
|
@@ -104,11 +238,13 @@ function createSSEConnection(config, callbacks) {
|
|
|
104
238
|
return {
|
|
105
239
|
close: () => {
|
|
106
240
|
disposed = true;
|
|
107
|
-
if (reconnectTimer) {
|
|
108
|
-
clearTimeout(reconnectTimer);
|
|
109
|
-
reconnectTimer = null;
|
|
110
|
-
}
|
|
111
241
|
cleanup();
|
|
242
|
+
},
|
|
243
|
+
reconnectImmediate: () => {
|
|
244
|
+
if (disposed) return;
|
|
245
|
+
cleanup();
|
|
246
|
+
attempt = 0;
|
|
247
|
+
connect();
|
|
112
248
|
}
|
|
113
249
|
};
|
|
114
250
|
}
|
|
@@ -116,23 +252,46 @@ function createSSEConnection(config, callbacks) {
|
|
|
116
252
|
// src/session.ts
|
|
117
253
|
var DEFAULT_RECONNECT_DELAY_MS2 = 3e3;
|
|
118
254
|
var MAX_SEEN_IDS = 500;
|
|
119
|
-
|
|
255
|
+
var MARK_READ_RETRY_CONFIG = {
|
|
256
|
+
maxAttempts: 2,
|
|
257
|
+
baseDelayMs: 500,
|
|
258
|
+
maxDelayMs: 2e3,
|
|
259
|
+
jitterFactor: 0.3
|
|
260
|
+
};
|
|
261
|
+
function parseRetryAfter(res) {
|
|
262
|
+
const header = res.headers.get("Retry-After");
|
|
263
|
+
if (!header) return void 0;
|
|
264
|
+
const seconds = Number(header);
|
|
265
|
+
return Number.isFinite(seconds) ? seconds : void 0;
|
|
266
|
+
}
|
|
267
|
+
async function throwHttpError(res) {
|
|
268
|
+
const body = await res.text().catch(() => "");
|
|
269
|
+
throw new HttpError(res.status, body, parseRetryAfter(res));
|
|
270
|
+
}
|
|
271
|
+
async function createChatSession(config, channelId, profile, callbacks, networkMonitor) {
|
|
120
272
|
const serviceUrl = resolveServerUrl(config.manifest, channelId);
|
|
121
273
|
const customHeaders = config.headers ?? {};
|
|
122
274
|
const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS2;
|
|
275
|
+
const retryConfig = resolveRetryConfig(config.resilience);
|
|
276
|
+
const sessionAbort = new AbortController();
|
|
123
277
|
callbacks.onStatusChange("connecting");
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
278
|
+
const joinFn = async () => {
|
|
279
|
+
const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
282
|
+
body: JSON.stringify(profile),
|
|
283
|
+
signal: sessionAbort.signal
|
|
284
|
+
});
|
|
285
|
+
if (joinRes.status === 410) {
|
|
286
|
+
throw new ChannelClosedError(channelId);
|
|
287
|
+
}
|
|
288
|
+
if (!joinRes.ok) {
|
|
289
|
+
await throwHttpError(joinRes);
|
|
290
|
+
}
|
|
291
|
+
return joinRes.json();
|
|
292
|
+
};
|
|
293
|
+
const joinData = retryConfig ? await withRetry(joinFn, retryConfig, sessionAbort.signal) : await joinFn();
|
|
294
|
+
const { messages, participants, joined_at } = joinData;
|
|
136
295
|
let lastEventId = messages.length > 0 ? messages[messages.length - 1].id : void 0;
|
|
137
296
|
const joinedAt = joined_at;
|
|
138
297
|
const seenMessageIds = new Set(messages.map((m) => m.id));
|
|
@@ -156,7 +315,8 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
156
315
|
headers: customHeaders,
|
|
157
316
|
reconnectDelayMs: reconnectDelay,
|
|
158
317
|
lastEventId,
|
|
159
|
-
customEvents: ["resync"]
|
|
318
|
+
customEvents: ["resync"],
|
|
319
|
+
networkMonitor
|
|
160
320
|
},
|
|
161
321
|
{
|
|
162
322
|
onOpen: () => {
|
|
@@ -180,8 +340,6 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
180
340
|
trimSeenIds();
|
|
181
341
|
callbacks.onMessage(message);
|
|
182
342
|
} else if (eventType === "resync") {
|
|
183
|
-
sseConn?.close();
|
|
184
|
-
sseConn = null;
|
|
185
343
|
callbacks.onResync();
|
|
186
344
|
}
|
|
187
345
|
},
|
|
@@ -204,44 +362,65 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
204
362
|
channelId,
|
|
205
363
|
initialParticipants: participants,
|
|
206
364
|
initialMessages: messages,
|
|
207
|
-
|
|
365
|
+
networkMonitor: networkMonitor ?? null,
|
|
366
|
+
sendMessage: async (type, body, attributes, idempotencyKey) => {
|
|
208
367
|
if (disposed) throw new ChatDisconnectedError("disconnected");
|
|
209
368
|
const payload = {
|
|
210
369
|
sender_id: profile.id,
|
|
211
370
|
type,
|
|
212
371
|
body,
|
|
213
|
-
attributes
|
|
372
|
+
attributes,
|
|
373
|
+
...idempotencyKey ? { idempotency_key: idempotencyKey } : {}
|
|
214
374
|
};
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
375
|
+
const sendFn = async () => {
|
|
376
|
+
const res = await fetch(
|
|
377
|
+
`${serviceUrl}/channels/${channelId}/messages`,
|
|
378
|
+
{
|
|
379
|
+
method: "POST",
|
|
380
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
381
|
+
body: JSON.stringify(payload),
|
|
382
|
+
signal: sessionAbort.signal
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
if (!res.ok) {
|
|
386
|
+
await throwHttpError(res);
|
|
221
387
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
const response = await res.json();
|
|
388
|
+
return res.json();
|
|
389
|
+
};
|
|
390
|
+
const response = retryConfig ? await withRetry(sendFn, retryConfig, sessionAbort.signal) : await sendFn();
|
|
227
391
|
seenMessageIds.add(response.id);
|
|
228
392
|
return response;
|
|
229
393
|
},
|
|
230
394
|
markAsRead: async (messageId) => {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
395
|
+
const readFn = async () => {
|
|
396
|
+
const res = await fetch(`${serviceUrl}/channels/${channelId}/read`, {
|
|
397
|
+
method: "POST",
|
|
398
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
399
|
+
body: JSON.stringify({
|
|
400
|
+
participant_id: profile.id,
|
|
401
|
+
message_id: messageId
|
|
402
|
+
}),
|
|
403
|
+
signal: sessionAbort.signal
|
|
404
|
+
});
|
|
405
|
+
if (!res.ok) {
|
|
406
|
+
await throwHttpError(res);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
if (retryConfig) {
|
|
410
|
+
try {
|
|
411
|
+
await withRetry(readFn, MARK_READ_RETRY_CONFIG, sessionAbort.signal);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
if (err instanceof HttpError) {
|
|
414
|
+
callbacks.onError(err);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
await readFn();
|
|
241
419
|
}
|
|
242
420
|
},
|
|
243
421
|
disconnect: () => {
|
|
244
422
|
disposed = true;
|
|
423
|
+
sessionAbort.abort();
|
|
245
424
|
sseConn?.close();
|
|
246
425
|
sseConn = null;
|
|
247
426
|
callbacks.onStatusChange("disconnected");
|
|
@@ -249,13 +428,342 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
249
428
|
};
|
|
250
429
|
}
|
|
251
430
|
|
|
431
|
+
// src/network-monitor.ts
|
|
432
|
+
function createStubMonitor() {
|
|
433
|
+
return {
|
|
434
|
+
isConnected: () => true,
|
|
435
|
+
subscribe: () => () => {
|
|
436
|
+
},
|
|
437
|
+
waitForOnline: () => Promise.resolve(),
|
|
438
|
+
dispose: () => {
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
var resolvedNetInfo = void 0;
|
|
443
|
+
var netInfoResolved = false;
|
|
444
|
+
function getNetInfo() {
|
|
445
|
+
if (netInfoResolved) return resolvedNetInfo;
|
|
446
|
+
netInfoResolved = true;
|
|
447
|
+
try {
|
|
448
|
+
resolvedNetInfo = __require("@react-native-community/netinfo");
|
|
449
|
+
} catch {
|
|
450
|
+
resolvedNetInfo = null;
|
|
451
|
+
}
|
|
452
|
+
return resolvedNetInfo;
|
|
453
|
+
}
|
|
454
|
+
function createNetworkMonitor() {
|
|
455
|
+
const NetInfo = getNetInfo();
|
|
456
|
+
if (!NetInfo) return createStubMonitor();
|
|
457
|
+
const netInfoModule = NetInfo.default ?? NetInfo;
|
|
458
|
+
let connected = true;
|
|
459
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
460
|
+
let unsubscribeNetInfo = null;
|
|
461
|
+
unsubscribeNetInfo = netInfoModule.addEventListener(
|
|
462
|
+
(state) => {
|
|
463
|
+
const next = state.isConnected !== false;
|
|
464
|
+
if (next === connected) return;
|
|
465
|
+
connected = next;
|
|
466
|
+
for (const cb of listeners) {
|
|
467
|
+
cb(connected);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
);
|
|
471
|
+
return {
|
|
472
|
+
isConnected: () => connected,
|
|
473
|
+
subscribe: (cb) => {
|
|
474
|
+
listeners.add(cb);
|
|
475
|
+
return () => {
|
|
476
|
+
listeners.delete(cb);
|
|
477
|
+
};
|
|
478
|
+
},
|
|
479
|
+
waitForOnline: (signal) => {
|
|
480
|
+
if (connected) return Promise.resolve();
|
|
481
|
+
return new Promise((resolve, reject) => {
|
|
482
|
+
if (signal?.aborted) {
|
|
483
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const unsub = () => {
|
|
487
|
+
listeners.delete(handler);
|
|
488
|
+
signal?.removeEventListener("abort", onAbort);
|
|
489
|
+
};
|
|
490
|
+
const handler = (isOnline) => {
|
|
491
|
+
if (isOnline) {
|
|
492
|
+
unsub();
|
|
493
|
+
resolve();
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const onAbort = () => {
|
|
497
|
+
unsub();
|
|
498
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
499
|
+
};
|
|
500
|
+
listeners.add(handler);
|
|
501
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
502
|
+
});
|
|
503
|
+
},
|
|
504
|
+
dispose: () => {
|
|
505
|
+
listeners.clear();
|
|
506
|
+
unsubscribeNetInfo?.();
|
|
507
|
+
unsubscribeNetInfo = null;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/message-queue.ts
|
|
513
|
+
var resolvedStorage;
|
|
514
|
+
function tryRequire(name) {
|
|
515
|
+
try {
|
|
516
|
+
return __require(name);
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function createMmkvAdapter(mod) {
|
|
522
|
+
const MMKV = mod.MMKV ?? mod.default?.MMKV ?? mod;
|
|
523
|
+
const instance = new MMKV({ id: "chika-queue" });
|
|
524
|
+
return {
|
|
525
|
+
getItem: (key) => Promise.resolve(instance.getString(key) ?? null),
|
|
526
|
+
setItem: (key, value) => {
|
|
527
|
+
instance.set(key, value);
|
|
528
|
+
return Promise.resolve();
|
|
529
|
+
},
|
|
530
|
+
removeItem: (key) => {
|
|
531
|
+
instance.delete(key);
|
|
532
|
+
return Promise.resolve();
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function createAsyncStorageAdapterFrom(mod) {
|
|
537
|
+
const storage = mod.default ?? mod;
|
|
538
|
+
return {
|
|
539
|
+
getItem: (key) => storage.getItem(key),
|
|
540
|
+
setItem: (key, value) => storage.setItem(key, value),
|
|
541
|
+
removeItem: (key) => storage.removeItem(key)
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function createQueueStorage() {
|
|
545
|
+
if (resolvedStorage !== void 0) return resolvedStorage?.adapter ?? null;
|
|
546
|
+
const mmkv = tryRequire("react-native-mmkv");
|
|
547
|
+
if (mmkv) {
|
|
548
|
+
try {
|
|
549
|
+
const adapter = createMmkvAdapter(mmkv);
|
|
550
|
+
resolvedStorage = { type: "mmkv", adapter };
|
|
551
|
+
return adapter;
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const asyncStorage = tryRequire("@react-native-async-storage/async-storage");
|
|
556
|
+
if (asyncStorage) {
|
|
557
|
+
const adapter = createAsyncStorageAdapterFrom(asyncStorage);
|
|
558
|
+
resolvedStorage = { type: "async-storage", adapter };
|
|
559
|
+
return adapter;
|
|
560
|
+
}
|
|
561
|
+
resolvedStorage = null;
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
function createAsyncStorageAdapter() {
|
|
565
|
+
const mod = tryRequire("@react-native-async-storage/async-storage");
|
|
566
|
+
if (!mod) return null;
|
|
567
|
+
return createAsyncStorageAdapterFrom(mod);
|
|
568
|
+
}
|
|
569
|
+
var MessageQueue = class {
|
|
570
|
+
constructor(config) {
|
|
571
|
+
this.config = config;
|
|
572
|
+
this.storageKey = `chika_queue_${config.channelId}`;
|
|
573
|
+
this.unsubNetwork = config.networkMonitor.subscribe((connected) => {
|
|
574
|
+
if (connected) this.flush();
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
entries = [];
|
|
578
|
+
flushing = false;
|
|
579
|
+
unsubNetwork = null;
|
|
580
|
+
storageKey;
|
|
581
|
+
/**
|
|
582
|
+
* Restore queued messages from persistent storage on cold start.
|
|
583
|
+
* Restored entries are fire-and-forget — there is no caller awaiting their
|
|
584
|
+
* promise (the original enqueue() caller is gone after app restart).
|
|
585
|
+
* Success/failure is reported via onStatusChange/onError callbacks.
|
|
586
|
+
*/
|
|
587
|
+
async restore(rebuildSendFn) {
|
|
588
|
+
if (!this.config.storage) return;
|
|
589
|
+
try {
|
|
590
|
+
const raw = await this.config.storage.getItem(this.storageKey);
|
|
591
|
+
if (!raw) return;
|
|
592
|
+
const persisted = JSON.parse(raw);
|
|
593
|
+
for (const entry of persisted) {
|
|
594
|
+
const sendFn = rebuildSendFn(entry.optimisticId);
|
|
595
|
+
if (!sendFn) continue;
|
|
596
|
+
const abort = new AbortController();
|
|
597
|
+
this.entries.push({
|
|
598
|
+
optimisticId: entry.optimisticId,
|
|
599
|
+
sendFn,
|
|
600
|
+
status: "queued",
|
|
601
|
+
retryCount: entry.retryCount,
|
|
602
|
+
abort,
|
|
603
|
+
// No-op: restored entries have no caller awaiting the promise.
|
|
604
|
+
resolve: () => {
|
|
605
|
+
},
|
|
606
|
+
reject: () => {
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
if (this.entries.length > 0) {
|
|
611
|
+
this.config.onStatusChange?.();
|
|
612
|
+
this.flush();
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
this.config.onError?.(new Error("Failed to restore message queue from storage"));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
get pendingCount() {
|
|
619
|
+
return this.entries.length;
|
|
620
|
+
}
|
|
621
|
+
getAll() {
|
|
622
|
+
return this.entries.map((e) => ({
|
|
623
|
+
optimisticId: e.optimisticId,
|
|
624
|
+
status: e.status,
|
|
625
|
+
error: e.error,
|
|
626
|
+
retryCount: e.retryCount
|
|
627
|
+
}));
|
|
628
|
+
}
|
|
629
|
+
getStatus(optimisticId) {
|
|
630
|
+
const entry = this.entries.find((e) => e.optimisticId === optimisticId);
|
|
631
|
+
if (!entry) return void 0;
|
|
632
|
+
return {
|
|
633
|
+
optimisticId: entry.optimisticId,
|
|
634
|
+
status: entry.status,
|
|
635
|
+
error: entry.error,
|
|
636
|
+
retryCount: entry.retryCount
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
enqueue(sendFn, optimisticId) {
|
|
640
|
+
if (this.entries.length >= this.config.maxSize) {
|
|
641
|
+
throw new QueueFullError(this.config.maxSize);
|
|
642
|
+
}
|
|
643
|
+
const abort = new AbortController();
|
|
644
|
+
return new Promise((resolve, reject) => {
|
|
645
|
+
const entry = {
|
|
646
|
+
optimisticId,
|
|
647
|
+
sendFn,
|
|
648
|
+
status: "queued",
|
|
649
|
+
retryCount: 0,
|
|
650
|
+
abort,
|
|
651
|
+
resolve,
|
|
652
|
+
reject
|
|
653
|
+
};
|
|
654
|
+
this.entries.push(entry);
|
|
655
|
+
this.config.onStatusChange?.();
|
|
656
|
+
this.persist();
|
|
657
|
+
this.flush();
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
cancel(optimisticId) {
|
|
661
|
+
const idx = this.entries.findIndex((e) => e.optimisticId === optimisticId);
|
|
662
|
+
if (idx === -1) return;
|
|
663
|
+
const entry = this.entries[idx];
|
|
664
|
+
entry.abort.abort();
|
|
665
|
+
entry.reject(new DOMException("Cancelled", "AbortError"));
|
|
666
|
+
this.entries.splice(idx, 1);
|
|
667
|
+
this.config.onStatusChange?.();
|
|
668
|
+
this.persist();
|
|
669
|
+
}
|
|
670
|
+
retry(optimisticId) {
|
|
671
|
+
const entry = this.entries.find((e) => e.optimisticId === optimisticId);
|
|
672
|
+
if (!entry || entry.status !== "failed") return;
|
|
673
|
+
entry.status = "queued";
|
|
674
|
+
entry.error = void 0;
|
|
675
|
+
entry.abort = new AbortController();
|
|
676
|
+
this.config.onStatusChange?.();
|
|
677
|
+
this.flush();
|
|
678
|
+
}
|
|
679
|
+
dispose() {
|
|
680
|
+
this.unsubNetwork?.();
|
|
681
|
+
this.unsubNetwork = null;
|
|
682
|
+
for (const entry of this.entries) {
|
|
683
|
+
entry.abort.abort();
|
|
684
|
+
entry.reject(new DOMException("Queue disposed", "AbortError"));
|
|
685
|
+
}
|
|
686
|
+
this.entries = [];
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Trigger a flush of queued messages. Returns immediately if a flush is
|
|
690
|
+
* already in progress or the network is offline. The in-progress flush
|
|
691
|
+
* will pick up any newly queued entries via its while loop.
|
|
692
|
+
*/
|
|
693
|
+
async flush() {
|
|
694
|
+
if (this.flushing) return;
|
|
695
|
+
if (!this.config.networkMonitor.isConnected()) return;
|
|
696
|
+
this.flushing = true;
|
|
697
|
+
let awaitingSession = false;
|
|
698
|
+
try {
|
|
699
|
+
while (this.entries.length > 0) {
|
|
700
|
+
const entry = this.entries.find((e) => e.status === "queued");
|
|
701
|
+
if (!entry) break;
|
|
702
|
+
if (!this.config.networkMonitor.isConnected()) break;
|
|
703
|
+
entry.status = "sending";
|
|
704
|
+
this.config.onStatusChange?.();
|
|
705
|
+
try {
|
|
706
|
+
const result = await withRetry(
|
|
707
|
+
entry.sendFn,
|
|
708
|
+
this.config.retryConfig,
|
|
709
|
+
entry.abort.signal
|
|
710
|
+
);
|
|
711
|
+
entry.resolve(result);
|
|
712
|
+
const idx = this.entries.indexOf(entry);
|
|
713
|
+
if (idx !== -1) this.entries.splice(idx, 1);
|
|
714
|
+
this.config.onStatusChange?.();
|
|
715
|
+
this.persist();
|
|
716
|
+
} catch (err) {
|
|
717
|
+
if (entry.abort.signal.aborted) continue;
|
|
718
|
+
if (err instanceof ChatDisconnectedError) {
|
|
719
|
+
entry.status = "queued";
|
|
720
|
+
this.config.onStatusChange?.();
|
|
721
|
+
awaitingSession = true;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
entry.status = "failed";
|
|
725
|
+
entry.error = err instanceof Error ? err : new Error(String(err));
|
|
726
|
+
entry.retryCount++;
|
|
727
|
+
this.config.onStatusChange?.();
|
|
728
|
+
this.persist();
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
} finally {
|
|
732
|
+
this.flushing = false;
|
|
733
|
+
if (!awaitingSession && this.entries.some((e) => e.status === "queued") && this.config.networkMonitor.isConnected()) {
|
|
734
|
+
queueMicrotask(() => this.flush());
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
persist() {
|
|
739
|
+
if (!this.config.storage) return;
|
|
740
|
+
const data = this.entries.map((e) => ({
|
|
741
|
+
optimisticId: e.optimisticId,
|
|
742
|
+
retryCount: e.retryCount
|
|
743
|
+
}));
|
|
744
|
+
const onErr = (err) => {
|
|
745
|
+
this.config.onError?.(
|
|
746
|
+
err instanceof Error ? err : new Error("Queue storage write failed")
|
|
747
|
+
);
|
|
748
|
+
};
|
|
749
|
+
if (data.length === 0) {
|
|
750
|
+
this.config.storage.removeItem(this.storageKey).catch(onErr);
|
|
751
|
+
} else {
|
|
752
|
+
this.config.storage.setItem(this.storageKey, JSON.stringify(data)).catch(onErr);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
|
|
252
757
|
// src/use-chat.ts
|
|
253
758
|
var DEFAULT_BACKGROUND_GRACE_MS = 2e3;
|
|
759
|
+
var DEFAULT_MAX_QUEUE_SIZE = 50;
|
|
760
|
+
var queueRegistry = /* @__PURE__ */ new Map();
|
|
254
761
|
function useChat({ config, channelId, profile, onMessage }) {
|
|
255
762
|
const [messages, setMessages] = useState([]);
|
|
256
763
|
const [participants, setParticipants] = useState([]);
|
|
257
764
|
const [status, setStatus] = useState("connecting");
|
|
258
765
|
const [error, setError] = useState(null);
|
|
766
|
+
const [pendingMessages, setPendingMessages] = useState([]);
|
|
259
767
|
const sessionRef = useRef(null);
|
|
260
768
|
const disposedRef = useRef(false);
|
|
261
769
|
const messagesRef = useRef(messages);
|
|
@@ -272,10 +780,36 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
272
780
|
onMessageRef.current = onMessage;
|
|
273
781
|
const startingRef = useRef(false);
|
|
274
782
|
const pendingOptimisticIds = useRef(/* @__PURE__ */ new Set());
|
|
783
|
+
const [monitor, setMonitor] = useState(null);
|
|
784
|
+
const monitorRef = useRef(null);
|
|
785
|
+
monitorRef.current = monitor;
|
|
786
|
+
const queueRef = useRef(null);
|
|
787
|
+
const resilienceEnabled = config.resilience !== false;
|
|
788
|
+
const queueEnabled = resilienceEnabled && (typeof config.resilience === "object" ? config.resilience.offlineQueue !== false : true);
|
|
789
|
+
const retryConfig = resolveRetryConfig(config.resilience);
|
|
790
|
+
const maxQueueSize = (resilienceEnabled && config.resilience && typeof config.resilience === "object" ? config.resilience.maxQueueSize : void 0) ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
275
791
|
const backgroundGraceMs = config.backgroundGraceMs ?? (Platform.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS : 0);
|
|
792
|
+
const injectedMonitor = typeof config.resilience === "object" ? config.resilience.networkMonitor : void 0;
|
|
793
|
+
useEffect(() => {
|
|
794
|
+
if (!resilienceEnabled) {
|
|
795
|
+
setMonitor(null);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (injectedMonitor) {
|
|
799
|
+
setMonitor(injectedMonitor);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const m = createNetworkMonitor();
|
|
803
|
+
setMonitor(m);
|
|
804
|
+
return () => {
|
|
805
|
+
m.dispose();
|
|
806
|
+
setMonitor(null);
|
|
807
|
+
};
|
|
808
|
+
}, [resilienceEnabled, injectedMonitor]);
|
|
276
809
|
const callbacks = {
|
|
277
810
|
onMessage: (message) => {
|
|
278
811
|
if (disposedRef.current) return;
|
|
812
|
+
let matchedOptimisticId = null;
|
|
279
813
|
setMessages((prev) => {
|
|
280
814
|
if (pendingOptimisticIds.current.size === 0) {
|
|
281
815
|
return [...prev, message];
|
|
@@ -284,20 +818,28 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
284
818
|
(m) => pendingOptimisticIds.current.has(m.id) && m.sender_id === message.sender_id && m.body === message.body && m.type === message.type
|
|
285
819
|
);
|
|
286
820
|
if (optimisticIdx !== -1) {
|
|
287
|
-
|
|
288
|
-
pendingOptimisticIds.current.delete(
|
|
821
|
+
matchedOptimisticId = prev[optimisticIdx].id;
|
|
822
|
+
pendingOptimisticIds.current.delete(matchedOptimisticId);
|
|
289
823
|
const next = [...prev];
|
|
290
824
|
next[optimisticIdx] = message;
|
|
291
825
|
return next;
|
|
292
826
|
}
|
|
293
827
|
return [...prev, message];
|
|
294
828
|
});
|
|
829
|
+
if (matchedOptimisticId) {
|
|
830
|
+
const s = queueRef.current?.getStatus(matchedOptimisticId);
|
|
831
|
+
if (s && s.status !== "sending") {
|
|
832
|
+
queueRef.current?.cancel(matchedOptimisticId);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
295
835
|
onMessageRef.current?.(message);
|
|
296
836
|
},
|
|
297
837
|
onStatusChange: (nextStatus) => {
|
|
298
838
|
if (disposedRef.current) return;
|
|
299
839
|
setStatus(nextStatus);
|
|
300
|
-
if (nextStatus === "connected")
|
|
840
|
+
if (nextStatus === "connected") {
|
|
841
|
+
setError(null);
|
|
842
|
+
}
|
|
301
843
|
},
|
|
302
844
|
onError: (err) => {
|
|
303
845
|
if (disposedRef.current) return;
|
|
@@ -317,14 +859,29 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
317
859
|
sessionRef.current = null;
|
|
318
860
|
}
|
|
319
861
|
try {
|
|
320
|
-
const session = await createChatSession(
|
|
862
|
+
const session = await createChatSession(
|
|
863
|
+
configRef.current,
|
|
864
|
+
channelId,
|
|
865
|
+
profileRef.current,
|
|
866
|
+
callbacks,
|
|
867
|
+
monitorRef.current ?? void 0
|
|
868
|
+
);
|
|
321
869
|
if (disposedRef.current) {
|
|
322
870
|
session.disconnect();
|
|
323
871
|
return;
|
|
324
872
|
}
|
|
325
873
|
sessionRef.current = session;
|
|
326
874
|
setParticipants(session.initialParticipants);
|
|
327
|
-
|
|
875
|
+
queueRef.current?.flush();
|
|
876
|
+
if (pendingOptimisticIds.current.size > 0) {
|
|
877
|
+
const pendingIds = pendingOptimisticIds.current;
|
|
878
|
+
setMessages((prev) => {
|
|
879
|
+
const pendingMsgs = prev.filter((m) => pendingIds.has(m.id));
|
|
880
|
+
return [...session.initialMessages, ...pendingMsgs];
|
|
881
|
+
});
|
|
882
|
+
} else {
|
|
883
|
+
setMessages(session.initialMessages);
|
|
884
|
+
}
|
|
328
885
|
} catch (err) {
|
|
329
886
|
if (disposedRef.current) return;
|
|
330
887
|
if (err instanceof ChannelClosedError) {
|
|
@@ -340,6 +897,7 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
340
897
|
}
|
|
341
898
|
useEffect(() => {
|
|
342
899
|
disposedRef.current = false;
|
|
900
|
+
if (resilienceEnabled && !monitor) return;
|
|
343
901
|
startSession();
|
|
344
902
|
return () => {
|
|
345
903
|
disposedRef.current = true;
|
|
@@ -357,7 +915,52 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
357
915
|
sessionRef.current?.disconnect();
|
|
358
916
|
sessionRef.current = null;
|
|
359
917
|
};
|
|
360
|
-
}, [channelId]);
|
|
918
|
+
}, [channelId, monitor]);
|
|
919
|
+
useEffect(() => {
|
|
920
|
+
if (!queueEnabled || !monitor || !retryConfig) return;
|
|
921
|
+
let entry = queueRegistry.get(channelId);
|
|
922
|
+
if (!entry) {
|
|
923
|
+
const queue = new MessageQueue({
|
|
924
|
+
channelId,
|
|
925
|
+
maxSize: maxQueueSize,
|
|
926
|
+
retryConfig,
|
|
927
|
+
networkMonitor: monitor,
|
|
928
|
+
storage: typeof config.resilience === "object" ? config.resilience.queueStorage : void 0,
|
|
929
|
+
onError: (err) => {
|
|
930
|
+
if (!disposedRef.current) setError(err);
|
|
931
|
+
},
|
|
932
|
+
onStatusChange: () => {
|
|
933
|
+
if (!disposedRef.current) {
|
|
934
|
+
setPendingMessages(queueRef.current?.getAll() ?? []);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
entry = { queue, refCount: 0 };
|
|
939
|
+
queueRegistry.set(channelId, entry);
|
|
940
|
+
}
|
|
941
|
+
entry.refCount++;
|
|
942
|
+
queueRef.current = entry.queue;
|
|
943
|
+
return () => {
|
|
944
|
+
const e = queueRegistry.get(channelId);
|
|
945
|
+
if (e) {
|
|
946
|
+
e.refCount--;
|
|
947
|
+
if (e.refCount <= 0) {
|
|
948
|
+
e.queue.dispose();
|
|
949
|
+
queueRegistry.delete(channelId);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
queueRef.current = null;
|
|
953
|
+
};
|
|
954
|
+
}, [channelId, queueEnabled, monitor]);
|
|
955
|
+
useEffect(() => {
|
|
956
|
+
if (!monitor || !resilienceEnabled) return;
|
|
957
|
+
const unsub = monitor.subscribe((connected) => {
|
|
958
|
+
if (connected && statusRef.current === "error" && !startingRef.current) {
|
|
959
|
+
startSession();
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
return unsub;
|
|
963
|
+
}, [channelId, resilienceEnabled, monitor]);
|
|
361
964
|
useEffect(() => {
|
|
362
965
|
function teardownSession() {
|
|
363
966
|
sessionRef.current?.disconnect();
|
|
@@ -403,12 +1006,11 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
403
1006
|
const session = sessionRef.current;
|
|
404
1007
|
if (!session) throw new ChatDisconnectedError(statusRef.current);
|
|
405
1008
|
const optimistic = configRef.current.optimisticSend !== false;
|
|
406
|
-
|
|
1009
|
+
const messageKey = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
407
1010
|
if (optimistic) {
|
|
408
|
-
|
|
409
|
-
pendingOptimisticIds.current.add(optimisticId);
|
|
1011
|
+
pendingOptimisticIds.current.add(messageKey);
|
|
410
1012
|
const provisionalMsg = {
|
|
411
|
-
id:
|
|
1013
|
+
id: messageKey,
|
|
412
1014
|
channel_id: channelId,
|
|
413
1015
|
sender_id: profileRef.current.id,
|
|
414
1016
|
sender_role: profileRef.current.role,
|
|
@@ -419,35 +1021,91 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
419
1021
|
};
|
|
420
1022
|
setMessages((prev) => [...prev, provisionalMsg]);
|
|
421
1023
|
}
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
425
|
-
|
|
1024
|
+
const doSend = () => {
|
|
1025
|
+
const s = sessionRef.current;
|
|
1026
|
+
if (!s) throw new ChatDisconnectedError(statusRef.current);
|
|
1027
|
+
return s.sendMessage(type, body, attributes, messageKey);
|
|
1028
|
+
};
|
|
1029
|
+
const handleSuccess = (response) => {
|
|
1030
|
+
if (optimistic) {
|
|
1031
|
+
pendingOptimisticIds.current.delete(messageKey);
|
|
426
1032
|
setMessages((prev) => {
|
|
427
|
-
const stillPending = prev.some((m) => m.id ===
|
|
1033
|
+
const stillPending = prev.some((m) => m.id === messageKey);
|
|
428
1034
|
if (!stillPending) return prev;
|
|
429
1035
|
return prev.map(
|
|
430
|
-
(m) => m.id ===
|
|
1036
|
+
(m) => m.id === messageKey ? { ...m, id: response.id, created_at: response.created_at } : m
|
|
431
1037
|
);
|
|
432
1038
|
});
|
|
433
1039
|
}
|
|
1040
|
+
};
|
|
1041
|
+
const handleError = (_err) => {
|
|
1042
|
+
if (optimistic) {
|
|
1043
|
+
pendingOptimisticIds.current.delete(messageKey);
|
|
1044
|
+
setMessages((prev) => prev.filter((m) => m.id !== messageKey));
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
if (queueRef.current) {
|
|
1048
|
+
try {
|
|
1049
|
+
const response = await queueRef.current.enqueue(doSend, messageKey);
|
|
1050
|
+
handleSuccess(response);
|
|
1051
|
+
return response;
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
if (err instanceof QueueFullError) {
|
|
1054
|
+
handleError(err);
|
|
1055
|
+
throw err;
|
|
1056
|
+
}
|
|
1057
|
+
if (err instanceof RetryExhaustedError || !isRetryableError(err)) {
|
|
1058
|
+
if (optimistic && err instanceof RetryExhaustedError) {
|
|
1059
|
+
} else {
|
|
1060
|
+
handleError(err);
|
|
1061
|
+
}
|
|
1062
|
+
throw err;
|
|
1063
|
+
}
|
|
1064
|
+
handleError(err);
|
|
1065
|
+
throw err;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
const response = await doSend();
|
|
1070
|
+
handleSuccess(response);
|
|
434
1071
|
return response;
|
|
435
1072
|
} catch (err) {
|
|
436
|
-
|
|
437
|
-
pendingOptimisticIds.current.delete(optimisticId);
|
|
438
|
-
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
439
|
-
}
|
|
1073
|
+
handleError(err);
|
|
440
1074
|
throw err;
|
|
441
1075
|
}
|
|
442
1076
|
},
|
|
443
1077
|
[channelId]
|
|
444
1078
|
);
|
|
1079
|
+
const cancelMessage = useCallback(
|
|
1080
|
+
(optimisticId) => {
|
|
1081
|
+
queueRef.current?.cancel(optimisticId);
|
|
1082
|
+
pendingOptimisticIds.current.delete(optimisticId);
|
|
1083
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
1084
|
+
},
|
|
1085
|
+
[]
|
|
1086
|
+
);
|
|
1087
|
+
const retryMessage = useCallback(
|
|
1088
|
+
(optimisticId) => {
|
|
1089
|
+
queueRef.current?.retry(optimisticId);
|
|
1090
|
+
},
|
|
1091
|
+
[]
|
|
1092
|
+
);
|
|
445
1093
|
const disconnect = useCallback(() => {
|
|
446
1094
|
sessionRef.current?.disconnect();
|
|
447
1095
|
sessionRef.current = null;
|
|
448
1096
|
setStatus("disconnected");
|
|
449
1097
|
}, []);
|
|
450
|
-
return {
|
|
1098
|
+
return {
|
|
1099
|
+
messages,
|
|
1100
|
+
participants,
|
|
1101
|
+
status,
|
|
1102
|
+
error,
|
|
1103
|
+
sendMessage,
|
|
1104
|
+
disconnect,
|
|
1105
|
+
pendingMessages,
|
|
1106
|
+
cancelMessage,
|
|
1107
|
+
retryMessage
|
|
1108
|
+
};
|
|
451
1109
|
}
|
|
452
1110
|
|
|
453
1111
|
// src/use-unread.ts
|
|
@@ -466,6 +1124,25 @@ function useUnread(options) {
|
|
|
466
1124
|
const appStateRef = useRef2(AppState2.currentState);
|
|
467
1125
|
const backgroundTimerRef = useRef2(null);
|
|
468
1126
|
const backgroundGraceMs = config.backgroundGraceMs ?? (Platform2.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS2 : 0);
|
|
1127
|
+
const [monitor, setMonitor] = useState2(null);
|
|
1128
|
+
const resilienceEnabled = config.resilience !== false;
|
|
1129
|
+
const injectedMonitor = typeof config.resilience === "object" ? config.resilience.networkMonitor : void 0;
|
|
1130
|
+
useEffect2(() => {
|
|
1131
|
+
if (!resilienceEnabled) {
|
|
1132
|
+
setMonitor(null);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (injectedMonitor) {
|
|
1136
|
+
setMonitor(injectedMonitor);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const m = createNetworkMonitor();
|
|
1140
|
+
setMonitor(m);
|
|
1141
|
+
return () => {
|
|
1142
|
+
m.dispose();
|
|
1143
|
+
setMonitor(null);
|
|
1144
|
+
};
|
|
1145
|
+
}, [resilienceEnabled, injectedMonitor]);
|
|
469
1146
|
const connect = useCallback2(() => {
|
|
470
1147
|
connRef.current?.close();
|
|
471
1148
|
connRef.current = null;
|
|
@@ -477,7 +1154,8 @@ function useUnread(options) {
|
|
|
477
1154
|
url,
|
|
478
1155
|
headers: customHeaders,
|
|
479
1156
|
reconnectDelayMs: configRef.current.reconnectDelayMs,
|
|
480
|
-
customEvents: UNREAD_CUSTOM_EVENTS
|
|
1157
|
+
customEvents: UNREAD_CUSTOM_EVENTS,
|
|
1158
|
+
networkMonitor: monitor ?? void 0
|
|
481
1159
|
},
|
|
482
1160
|
{
|
|
483
1161
|
onOpen: () => {
|
|
@@ -509,7 +1187,7 @@ function useUnread(options) {
|
|
|
509
1187
|
}
|
|
510
1188
|
}
|
|
511
1189
|
);
|
|
512
|
-
}, [channelId, participantId]);
|
|
1190
|
+
}, [channelId, participantId, monitor]);
|
|
513
1191
|
const disconnect = useCallback2(() => {
|
|
514
1192
|
connRef.current?.close();
|
|
515
1193
|
connRef.current = null;
|
|
@@ -522,6 +1200,7 @@ function useUnread(options) {
|
|
|
522
1200
|
disconnect();
|
|
523
1201
|
return;
|
|
524
1202
|
}
|
|
1203
|
+
if (resilienceEnabled && !monitor) return;
|
|
525
1204
|
connect();
|
|
526
1205
|
return () => {
|
|
527
1206
|
disconnect();
|
|
@@ -572,11 +1251,21 @@ function useUnread(options) {
|
|
|
572
1251
|
export {
|
|
573
1252
|
ChannelClosedError,
|
|
574
1253
|
ChatDisconnectedError,
|
|
1254
|
+
HttpError,
|
|
1255
|
+
QueueFullError,
|
|
1256
|
+
RetryExhaustedError,
|
|
1257
|
+
calculateBackoff,
|
|
1258
|
+
createAsyncStorageAdapter,
|
|
575
1259
|
createChatSession,
|
|
576
1260
|
createManifest,
|
|
1261
|
+
createNetworkMonitor,
|
|
1262
|
+
createQueueStorage,
|
|
577
1263
|
createSSEConnection,
|
|
1264
|
+
isRetryableError,
|
|
1265
|
+
resolveRetryConfig,
|
|
578
1266
|
resolveServerUrl,
|
|
579
1267
|
useChat,
|
|
580
|
-
useUnread
|
|
1268
|
+
useUnread,
|
|
1269
|
+
withRetry
|
|
581
1270
|
};
|
|
582
1271
|
//# sourceMappingURL=index.mjs.map
|