@pedi/chika-sdk 1.0.5 → 1.0.6
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 +739 -70
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +735 -69
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -3
package/dist/index.js
CHANGED
|
@@ -32,12 +32,22 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
ChannelClosedError: () => ChannelClosedError,
|
|
34
34
|
ChatDisconnectedError: () => ChatDisconnectedError,
|
|
35
|
+
HttpError: () => HttpError,
|
|
36
|
+
QueueFullError: () => QueueFullError,
|
|
37
|
+
RetryExhaustedError: () => RetryExhaustedError,
|
|
38
|
+
calculateBackoff: () => calculateBackoff,
|
|
39
|
+
createAsyncStorageAdapter: () => createAsyncStorageAdapter,
|
|
35
40
|
createChatSession: () => createChatSession,
|
|
36
41
|
createManifest: () => createManifest,
|
|
42
|
+
createNetworkMonitor: () => createNetworkMonitor,
|
|
43
|
+
createQueueStorage: () => createQueueStorage,
|
|
37
44
|
createSSEConnection: () => createSSEConnection,
|
|
45
|
+
isRetryableError: () => isRetryableError,
|
|
46
|
+
resolveRetryConfig: () => resolveRetryConfig,
|
|
38
47
|
resolveServerUrl: () => resolveServerUrl,
|
|
39
48
|
useChat: () => useChat,
|
|
40
|
-
useUnread: () => useUnread
|
|
49
|
+
useUnread: () => useUnread,
|
|
50
|
+
withRetry: () => withRetry
|
|
41
51
|
});
|
|
42
52
|
module.exports = __toCommonJS(index_exports);
|
|
43
53
|
|
|
@@ -60,6 +70,104 @@ var ChannelClosedError = class extends Error {
|
|
|
60
70
|
this.name = "ChannelClosedError";
|
|
61
71
|
}
|
|
62
72
|
};
|
|
73
|
+
var HttpError = class extends Error {
|
|
74
|
+
constructor(status, body, retryAfter) {
|
|
75
|
+
super(`HTTP ${status}: ${body}`);
|
|
76
|
+
this.status = status;
|
|
77
|
+
this.body = body;
|
|
78
|
+
this.retryAfter = retryAfter;
|
|
79
|
+
this.name = "HttpError";
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
var RetryExhaustedError = class extends Error {
|
|
83
|
+
constructor(operation, attempts, lastError) {
|
|
84
|
+
super(`${operation} failed after ${attempts} attempts: ${lastError.message}`);
|
|
85
|
+
this.operation = operation;
|
|
86
|
+
this.attempts = attempts;
|
|
87
|
+
this.lastError = lastError;
|
|
88
|
+
this.name = "RetryExhaustedError";
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var QueueFullError = class extends Error {
|
|
92
|
+
constructor(maxSize) {
|
|
93
|
+
super(`Message queue full (max ${maxSize})`);
|
|
94
|
+
this.maxSize = maxSize;
|
|
95
|
+
this.name = "QueueFullError";
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// src/retry.ts
|
|
100
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
101
|
+
maxAttempts: 3,
|
|
102
|
+
baseDelayMs: 1e3,
|
|
103
|
+
maxDelayMs: 1e4,
|
|
104
|
+
jitterFactor: 0.3
|
|
105
|
+
};
|
|
106
|
+
function calculateBackoff(attempt, config) {
|
|
107
|
+
const delay = Math.min(config.baseDelayMs * 2 ** attempt, config.maxDelayMs);
|
|
108
|
+
const jitter = 1 + (Math.random() * 2 - 1) * config.jitterFactor;
|
|
109
|
+
return Math.round(delay * jitter);
|
|
110
|
+
}
|
|
111
|
+
function isRetryableError(error) {
|
|
112
|
+
if (error instanceof ChannelClosedError) return false;
|
|
113
|
+
if (error instanceof ChatDisconnectedError) return false;
|
|
114
|
+
if (error instanceof QueueFullError) return false;
|
|
115
|
+
if (error instanceof DOMException && error.name === "AbortError") return false;
|
|
116
|
+
if (error instanceof HttpError) {
|
|
117
|
+
const { status } = error;
|
|
118
|
+
if (status === 408 || status === 429) return true;
|
|
119
|
+
if (status >= 500) return true;
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (error instanceof TypeError) return true;
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function sleep(ms, signal) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
if (signal?.aborted) {
|
|
128
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const timer = setTimeout(resolve, ms);
|
|
132
|
+
signal?.addEventListener(
|
|
133
|
+
"abort",
|
|
134
|
+
() => {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
137
|
+
},
|
|
138
|
+
{ once: true }
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function resolveRetryConfig(resilience) {
|
|
143
|
+
if (resilience === false) return null;
|
|
144
|
+
if (!resilience || resilience.retry === void 0) return DEFAULT_RETRY_CONFIG;
|
|
145
|
+
if (resilience.retry === false) return null;
|
|
146
|
+
return { ...DEFAULT_RETRY_CONFIG, ...resilience.retry };
|
|
147
|
+
}
|
|
148
|
+
async function withRetry(fn, config = DEFAULT_RETRY_CONFIG, signal) {
|
|
149
|
+
let lastError;
|
|
150
|
+
for (let attempt = 0; attempt < config.maxAttempts; attempt++) {
|
|
151
|
+
if (signal?.aborted) {
|
|
152
|
+
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
return await fn();
|
|
156
|
+
} catch (err) {
|
|
157
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
158
|
+
if (!isRetryableError(err)) throw lastError;
|
|
159
|
+
if (attempt < config.maxAttempts - 1) {
|
|
160
|
+
const delayMs = err instanceof HttpError && err.retryAfter != null ? err.retryAfter * 1e3 : calculateBackoff(attempt, config);
|
|
161
|
+
await sleep(delayMs, signal);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw new RetryExhaustedError(
|
|
166
|
+
"operation",
|
|
167
|
+
config.maxAttempts,
|
|
168
|
+
lastError
|
|
169
|
+
);
|
|
170
|
+
}
|
|
63
171
|
|
|
64
172
|
// src/resolve-url.ts
|
|
65
173
|
function createManifest(serverUrl) {
|
|
@@ -78,27 +186,55 @@ function resolveServerUrl(manifest, channelId) {
|
|
|
78
186
|
var import_react_native_sse = __toESM(require("react-native-sse"));
|
|
79
187
|
var DEFAULT_RECONNECT_DELAY_MS = 3e3;
|
|
80
188
|
function createSSEConnection(config, callbacks) {
|
|
81
|
-
const
|
|
189
|
+
const baseDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;
|
|
82
190
|
const customEvents = config.customEvents ?? [];
|
|
191
|
+
const monitor = config.networkMonitor;
|
|
192
|
+
const backoffConfig = {
|
|
193
|
+
maxAttempts: Infinity,
|
|
194
|
+
baseDelayMs: baseDelay,
|
|
195
|
+
maxDelayMs: 3e4,
|
|
196
|
+
jitterFactor: 0.3
|
|
197
|
+
};
|
|
83
198
|
let currentLastEventId = config.lastEventId;
|
|
84
199
|
let es = null;
|
|
85
200
|
let disposed = false;
|
|
86
201
|
let reconnectTimer = null;
|
|
202
|
+
let attempt = 0;
|
|
203
|
+
let waitAbort = null;
|
|
87
204
|
function cleanup() {
|
|
88
205
|
if (es) {
|
|
89
206
|
es.removeAllEventListeners();
|
|
90
207
|
es.close();
|
|
91
208
|
es = null;
|
|
92
209
|
}
|
|
210
|
+
if (reconnectTimer) {
|
|
211
|
+
clearTimeout(reconnectTimer);
|
|
212
|
+
reconnectTimer = null;
|
|
213
|
+
}
|
|
214
|
+
if (waitAbort) {
|
|
215
|
+
waitAbort.abort();
|
|
216
|
+
waitAbort = null;
|
|
217
|
+
}
|
|
93
218
|
}
|
|
94
|
-
function scheduleReconnect() {
|
|
95
|
-
if (disposed || reconnectTimer) return;
|
|
219
|
+
async function scheduleReconnect() {
|
|
220
|
+
if (disposed || reconnectTimer || waitAbort) return;
|
|
96
221
|
callbacks.onReconnecting?.();
|
|
97
222
|
cleanup();
|
|
223
|
+
if (monitor && !monitor.isConnected()) {
|
|
224
|
+
waitAbort = new AbortController();
|
|
225
|
+
try {
|
|
226
|
+
await monitor.waitForOnline(waitAbort.signal);
|
|
227
|
+
} catch {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
waitAbort = null;
|
|
231
|
+
if (disposed) return;
|
|
232
|
+
}
|
|
233
|
+
const delay = calculateBackoff(attempt++, backoffConfig);
|
|
98
234
|
reconnectTimer = setTimeout(() => {
|
|
99
235
|
reconnectTimer = null;
|
|
100
236
|
connect();
|
|
101
|
-
},
|
|
237
|
+
}, delay);
|
|
102
238
|
}
|
|
103
239
|
function connect() {
|
|
104
240
|
if (disposed) return;
|
|
@@ -111,6 +247,7 @@ function createSSEConnection(config, callbacks) {
|
|
|
111
247
|
});
|
|
112
248
|
es.addEventListener("open", () => {
|
|
113
249
|
if (disposed) return;
|
|
250
|
+
attempt = 0;
|
|
114
251
|
callbacks.onOpen?.();
|
|
115
252
|
});
|
|
116
253
|
es.addEventListener("message", (event) => {
|
|
@@ -147,11 +284,13 @@ function createSSEConnection(config, callbacks) {
|
|
|
147
284
|
return {
|
|
148
285
|
close: () => {
|
|
149
286
|
disposed = true;
|
|
150
|
-
if (reconnectTimer) {
|
|
151
|
-
clearTimeout(reconnectTimer);
|
|
152
|
-
reconnectTimer = null;
|
|
153
|
-
}
|
|
154
287
|
cleanup();
|
|
288
|
+
},
|
|
289
|
+
reconnectImmediate: () => {
|
|
290
|
+
if (disposed) return;
|
|
291
|
+
cleanup();
|
|
292
|
+
attempt = 0;
|
|
293
|
+
connect();
|
|
155
294
|
}
|
|
156
295
|
};
|
|
157
296
|
}
|
|
@@ -159,23 +298,46 @@ function createSSEConnection(config, callbacks) {
|
|
|
159
298
|
// src/session.ts
|
|
160
299
|
var DEFAULT_RECONNECT_DELAY_MS2 = 3e3;
|
|
161
300
|
var MAX_SEEN_IDS = 500;
|
|
162
|
-
|
|
301
|
+
var MARK_READ_RETRY_CONFIG = {
|
|
302
|
+
maxAttempts: 2,
|
|
303
|
+
baseDelayMs: 500,
|
|
304
|
+
maxDelayMs: 2e3,
|
|
305
|
+
jitterFactor: 0.3
|
|
306
|
+
};
|
|
307
|
+
function parseRetryAfter(res) {
|
|
308
|
+
const header = res.headers.get("Retry-After");
|
|
309
|
+
if (!header) return void 0;
|
|
310
|
+
const seconds = Number(header);
|
|
311
|
+
return Number.isFinite(seconds) ? seconds : void 0;
|
|
312
|
+
}
|
|
313
|
+
async function throwHttpError(res) {
|
|
314
|
+
const body = await res.text().catch(() => "");
|
|
315
|
+
throw new HttpError(res.status, body, parseRetryAfter(res));
|
|
316
|
+
}
|
|
317
|
+
async function createChatSession(config, channelId, profile, callbacks, networkMonitor) {
|
|
163
318
|
const serviceUrl = resolveServerUrl(config.manifest, channelId);
|
|
164
319
|
const customHeaders = config.headers ?? {};
|
|
165
320
|
const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS2;
|
|
321
|
+
const retryConfig = resolveRetryConfig(config.resilience);
|
|
322
|
+
const sessionAbort = new AbortController();
|
|
166
323
|
callbacks.onStatusChange("connecting");
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
324
|
+
const joinFn = async () => {
|
|
325
|
+
const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
328
|
+
body: JSON.stringify(profile),
|
|
329
|
+
signal: sessionAbort.signal
|
|
330
|
+
});
|
|
331
|
+
if (joinRes.status === 410) {
|
|
332
|
+
throw new ChannelClosedError(channelId);
|
|
333
|
+
}
|
|
334
|
+
if (!joinRes.ok) {
|
|
335
|
+
await throwHttpError(joinRes);
|
|
336
|
+
}
|
|
337
|
+
return joinRes.json();
|
|
338
|
+
};
|
|
339
|
+
const joinData = retryConfig ? await withRetry(joinFn, retryConfig, sessionAbort.signal) : await joinFn();
|
|
340
|
+
const { messages, participants, joined_at } = joinData;
|
|
179
341
|
let lastEventId = messages.length > 0 ? messages[messages.length - 1].id : void 0;
|
|
180
342
|
const joinedAt = joined_at;
|
|
181
343
|
const seenMessageIds = new Set(messages.map((m) => m.id));
|
|
@@ -199,7 +361,8 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
199
361
|
headers: customHeaders,
|
|
200
362
|
reconnectDelayMs: reconnectDelay,
|
|
201
363
|
lastEventId,
|
|
202
|
-
customEvents: ["resync"]
|
|
364
|
+
customEvents: ["resync"],
|
|
365
|
+
networkMonitor
|
|
203
366
|
},
|
|
204
367
|
{
|
|
205
368
|
onOpen: () => {
|
|
@@ -223,8 +386,6 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
223
386
|
trimSeenIds();
|
|
224
387
|
callbacks.onMessage(message);
|
|
225
388
|
} else if (eventType === "resync") {
|
|
226
|
-
sseConn?.close();
|
|
227
|
-
sseConn = null;
|
|
228
389
|
callbacks.onResync();
|
|
229
390
|
}
|
|
230
391
|
},
|
|
@@ -247,44 +408,65 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
247
408
|
channelId,
|
|
248
409
|
initialParticipants: participants,
|
|
249
410
|
initialMessages: messages,
|
|
250
|
-
|
|
411
|
+
networkMonitor: networkMonitor ?? null,
|
|
412
|
+
sendMessage: async (type, body, attributes, idempotencyKey) => {
|
|
251
413
|
if (disposed) throw new ChatDisconnectedError("disconnected");
|
|
252
414
|
const payload = {
|
|
253
415
|
sender_id: profile.id,
|
|
254
416
|
type,
|
|
255
417
|
body,
|
|
256
|
-
attributes
|
|
418
|
+
attributes,
|
|
419
|
+
...idempotencyKey ? { idempotency_key: idempotencyKey } : {}
|
|
257
420
|
};
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
421
|
+
const sendFn = async () => {
|
|
422
|
+
const res = await fetch(
|
|
423
|
+
`${serviceUrl}/channels/${channelId}/messages`,
|
|
424
|
+
{
|
|
425
|
+
method: "POST",
|
|
426
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
427
|
+
body: JSON.stringify(payload),
|
|
428
|
+
signal: sessionAbort.signal
|
|
429
|
+
}
|
|
430
|
+
);
|
|
431
|
+
if (!res.ok) {
|
|
432
|
+
await throwHttpError(res);
|
|
264
433
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
const response = await res.json();
|
|
434
|
+
return res.json();
|
|
435
|
+
};
|
|
436
|
+
const response = retryConfig ? await withRetry(sendFn, retryConfig, sessionAbort.signal) : await sendFn();
|
|
270
437
|
seenMessageIds.add(response.id);
|
|
271
438
|
return response;
|
|
272
439
|
},
|
|
273
440
|
markAsRead: async (messageId) => {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
441
|
+
const readFn = async () => {
|
|
442
|
+
const res = await fetch(`${serviceUrl}/channels/${channelId}/read`, {
|
|
443
|
+
method: "POST",
|
|
444
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
445
|
+
body: JSON.stringify({
|
|
446
|
+
participant_id: profile.id,
|
|
447
|
+
message_id: messageId
|
|
448
|
+
}),
|
|
449
|
+
signal: sessionAbort.signal
|
|
450
|
+
});
|
|
451
|
+
if (!res.ok) {
|
|
452
|
+
await throwHttpError(res);
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
if (retryConfig) {
|
|
456
|
+
try {
|
|
457
|
+
await withRetry(readFn, MARK_READ_RETRY_CONFIG, sessionAbort.signal);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
if (err instanceof HttpError) {
|
|
460
|
+
callbacks.onError(err);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} else {
|
|
464
|
+
await readFn();
|
|
284
465
|
}
|
|
285
466
|
},
|
|
286
467
|
disconnect: () => {
|
|
287
468
|
disposed = true;
|
|
469
|
+
sessionAbort.abort();
|
|
288
470
|
sseConn?.close();
|
|
289
471
|
sseConn = null;
|
|
290
472
|
callbacks.onStatusChange("disconnected");
|
|
@@ -292,13 +474,327 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
292
474
|
};
|
|
293
475
|
}
|
|
294
476
|
|
|
477
|
+
// src/network-monitor.ts
|
|
478
|
+
function createStubMonitor() {
|
|
479
|
+
return {
|
|
480
|
+
isConnected: () => true,
|
|
481
|
+
subscribe: () => () => {
|
|
482
|
+
},
|
|
483
|
+
waitForOnline: () => Promise.resolve(),
|
|
484
|
+
dispose: () => {
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
var resolvedNetInfo = void 0;
|
|
489
|
+
var netInfoResolved = false;
|
|
490
|
+
function getNetInfo() {
|
|
491
|
+
if (netInfoResolved) return resolvedNetInfo;
|
|
492
|
+
netInfoResolved = true;
|
|
493
|
+
try {
|
|
494
|
+
resolvedNetInfo = require("@react-native-community/netinfo");
|
|
495
|
+
} catch {
|
|
496
|
+
resolvedNetInfo = null;
|
|
497
|
+
}
|
|
498
|
+
return resolvedNetInfo;
|
|
499
|
+
}
|
|
500
|
+
function createNetworkMonitor() {
|
|
501
|
+
const NetInfo = getNetInfo();
|
|
502
|
+
if (!NetInfo) return createStubMonitor();
|
|
503
|
+
const netInfoModule = NetInfo.default ?? NetInfo;
|
|
504
|
+
let connected = true;
|
|
505
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
506
|
+
let unsubscribeNetInfo = null;
|
|
507
|
+
unsubscribeNetInfo = netInfoModule.addEventListener(
|
|
508
|
+
(state) => {
|
|
509
|
+
const next = state.isConnected !== false;
|
|
510
|
+
if (next === connected) return;
|
|
511
|
+
connected = next;
|
|
512
|
+
for (const cb of listeners) {
|
|
513
|
+
cb(connected);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
return {
|
|
518
|
+
isConnected: () => connected,
|
|
519
|
+
subscribe: (cb) => {
|
|
520
|
+
listeners.add(cb);
|
|
521
|
+
return () => {
|
|
522
|
+
listeners.delete(cb);
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
waitForOnline: (signal) => {
|
|
526
|
+
if (connected) return Promise.resolve();
|
|
527
|
+
return new Promise((resolve, reject) => {
|
|
528
|
+
if (signal?.aborted) {
|
|
529
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const unsub = () => {
|
|
533
|
+
listeners.delete(handler);
|
|
534
|
+
signal?.removeEventListener("abort", onAbort);
|
|
535
|
+
};
|
|
536
|
+
const handler = (isOnline) => {
|
|
537
|
+
if (isOnline) {
|
|
538
|
+
unsub();
|
|
539
|
+
resolve();
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
const onAbort = () => {
|
|
543
|
+
unsub();
|
|
544
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
545
|
+
};
|
|
546
|
+
listeners.add(handler);
|
|
547
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
548
|
+
});
|
|
549
|
+
},
|
|
550
|
+
dispose: () => {
|
|
551
|
+
listeners.clear();
|
|
552
|
+
unsubscribeNetInfo?.();
|
|
553
|
+
unsubscribeNetInfo = null;
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/message-queue.ts
|
|
559
|
+
var resolvedStorage;
|
|
560
|
+
function tryRequire(name) {
|
|
561
|
+
try {
|
|
562
|
+
return require(name);
|
|
563
|
+
} catch {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
function createMmkvAdapter(mod) {
|
|
568
|
+
const MMKV = mod.MMKV ?? mod.default?.MMKV ?? mod;
|
|
569
|
+
const instance = new MMKV({ id: "chika-queue" });
|
|
570
|
+
return {
|
|
571
|
+
getItem: (key) => Promise.resolve(instance.getString(key) ?? null),
|
|
572
|
+
setItem: (key, value) => {
|
|
573
|
+
instance.set(key, value);
|
|
574
|
+
return Promise.resolve();
|
|
575
|
+
},
|
|
576
|
+
removeItem: (key) => {
|
|
577
|
+
instance.delete(key);
|
|
578
|
+
return Promise.resolve();
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function createAsyncStorageAdapterFrom(mod) {
|
|
583
|
+
const storage = mod.default ?? mod;
|
|
584
|
+
return {
|
|
585
|
+
getItem: (key) => storage.getItem(key),
|
|
586
|
+
setItem: (key, value) => storage.setItem(key, value),
|
|
587
|
+
removeItem: (key) => storage.removeItem(key)
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function createQueueStorage() {
|
|
591
|
+
if (resolvedStorage !== void 0) return resolvedStorage?.adapter ?? null;
|
|
592
|
+
const mmkv = tryRequire("react-native-mmkv");
|
|
593
|
+
if (mmkv) {
|
|
594
|
+
try {
|
|
595
|
+
const adapter = createMmkvAdapter(mmkv);
|
|
596
|
+
resolvedStorage = { type: "mmkv", adapter };
|
|
597
|
+
return adapter;
|
|
598
|
+
} catch {
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const asyncStorage = tryRequire("@react-native-async-storage/async-storage");
|
|
602
|
+
if (asyncStorage) {
|
|
603
|
+
const adapter = createAsyncStorageAdapterFrom(asyncStorage);
|
|
604
|
+
resolvedStorage = { type: "async-storage", adapter };
|
|
605
|
+
return adapter;
|
|
606
|
+
}
|
|
607
|
+
resolvedStorage = null;
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
function createAsyncStorageAdapter() {
|
|
611
|
+
const mod = tryRequire("@react-native-async-storage/async-storage");
|
|
612
|
+
if (!mod) return null;
|
|
613
|
+
return createAsyncStorageAdapterFrom(mod);
|
|
614
|
+
}
|
|
615
|
+
var MessageQueue = class {
|
|
616
|
+
constructor(config) {
|
|
617
|
+
this.config = config;
|
|
618
|
+
this.storageKey = `chika_queue_${config.channelId}`;
|
|
619
|
+
this.unsubNetwork = config.networkMonitor.subscribe((connected) => {
|
|
620
|
+
if (connected) this.flush();
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
entries = [];
|
|
624
|
+
flushing = false;
|
|
625
|
+
unsubNetwork = null;
|
|
626
|
+
storageKey;
|
|
627
|
+
/**
|
|
628
|
+
* Restore queued messages from persistent storage on cold start.
|
|
629
|
+
* Restored entries are fire-and-forget — there is no caller awaiting their
|
|
630
|
+
* promise (the original enqueue() caller is gone after app restart).
|
|
631
|
+
* Success/failure is reported via onStatusChange/onError callbacks.
|
|
632
|
+
*/
|
|
633
|
+
async restore(rebuildSendFn) {
|
|
634
|
+
if (!this.config.storage) return;
|
|
635
|
+
try {
|
|
636
|
+
const raw = await this.config.storage.getItem(this.storageKey);
|
|
637
|
+
if (!raw) return;
|
|
638
|
+
const persisted = JSON.parse(raw);
|
|
639
|
+
for (const entry of persisted) {
|
|
640
|
+
const sendFn = rebuildSendFn(entry.optimisticId);
|
|
641
|
+
if (!sendFn) continue;
|
|
642
|
+
const abort = new AbortController();
|
|
643
|
+
this.entries.push({
|
|
644
|
+
optimisticId: entry.optimisticId,
|
|
645
|
+
sendFn,
|
|
646
|
+
status: "queued",
|
|
647
|
+
retryCount: entry.retryCount,
|
|
648
|
+
abort,
|
|
649
|
+
// No-op: restored entries have no caller awaiting the promise.
|
|
650
|
+
resolve: () => {
|
|
651
|
+
},
|
|
652
|
+
reject: () => {
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
if (this.entries.length > 0) {
|
|
657
|
+
this.config.onStatusChange?.();
|
|
658
|
+
this.flush();
|
|
659
|
+
}
|
|
660
|
+
} catch {
|
|
661
|
+
this.config.onError?.(new Error("Failed to restore message queue from storage"));
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
get pendingCount() {
|
|
665
|
+
return this.entries.length;
|
|
666
|
+
}
|
|
667
|
+
getAll() {
|
|
668
|
+
return this.entries.map((e) => ({
|
|
669
|
+
optimisticId: e.optimisticId,
|
|
670
|
+
status: e.status,
|
|
671
|
+
error: e.error,
|
|
672
|
+
retryCount: e.retryCount
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
getStatus(optimisticId) {
|
|
676
|
+
const entry = this.entries.find((e) => e.optimisticId === optimisticId);
|
|
677
|
+
if (!entry) return void 0;
|
|
678
|
+
return {
|
|
679
|
+
optimisticId: entry.optimisticId,
|
|
680
|
+
status: entry.status,
|
|
681
|
+
error: entry.error,
|
|
682
|
+
retryCount: entry.retryCount
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
enqueue(sendFn, optimisticId) {
|
|
686
|
+
if (this.entries.length >= this.config.maxSize) {
|
|
687
|
+
throw new QueueFullError(this.config.maxSize);
|
|
688
|
+
}
|
|
689
|
+
const abort = new AbortController();
|
|
690
|
+
return new Promise((resolve, reject) => {
|
|
691
|
+
const entry = {
|
|
692
|
+
optimisticId,
|
|
693
|
+
sendFn,
|
|
694
|
+
status: this.config.networkMonitor.isConnected() ? "sending" : "queued",
|
|
695
|
+
retryCount: 0,
|
|
696
|
+
abort,
|
|
697
|
+
resolve,
|
|
698
|
+
reject
|
|
699
|
+
};
|
|
700
|
+
this.entries.push(entry);
|
|
701
|
+
this.config.onStatusChange?.();
|
|
702
|
+
this.persist();
|
|
703
|
+
this.flush();
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
cancel(optimisticId) {
|
|
707
|
+
const idx = this.entries.findIndex((e) => e.optimisticId === optimisticId);
|
|
708
|
+
if (idx === -1) return;
|
|
709
|
+
const entry = this.entries[idx];
|
|
710
|
+
entry.abort.abort();
|
|
711
|
+
entry.reject(new DOMException("Cancelled", "AbortError"));
|
|
712
|
+
this.entries.splice(idx, 1);
|
|
713
|
+
this.config.onStatusChange?.();
|
|
714
|
+
this.persist();
|
|
715
|
+
}
|
|
716
|
+
retry(optimisticId) {
|
|
717
|
+
const entry = this.entries.find((e) => e.optimisticId === optimisticId);
|
|
718
|
+
if (!entry || entry.status !== "failed") return;
|
|
719
|
+
entry.status = "queued";
|
|
720
|
+
entry.error = void 0;
|
|
721
|
+
entry.abort = new AbortController();
|
|
722
|
+
this.config.onStatusChange?.();
|
|
723
|
+
this.flush();
|
|
724
|
+
}
|
|
725
|
+
dispose() {
|
|
726
|
+
this.unsubNetwork?.();
|
|
727
|
+
this.unsubNetwork = null;
|
|
728
|
+
for (const entry of this.entries) {
|
|
729
|
+
entry.abort.abort();
|
|
730
|
+
entry.reject(new DOMException("Queue disposed", "AbortError"));
|
|
731
|
+
}
|
|
732
|
+
this.entries = [];
|
|
733
|
+
}
|
|
734
|
+
async flush() {
|
|
735
|
+
if (this.flushing) return;
|
|
736
|
+
if (!this.config.networkMonitor.isConnected()) return;
|
|
737
|
+
this.flushing = true;
|
|
738
|
+
try {
|
|
739
|
+
while (this.entries.length > 0) {
|
|
740
|
+
const entry = this.entries.find((e) => e.status === "queued");
|
|
741
|
+
if (!entry) break;
|
|
742
|
+
if (!this.config.networkMonitor.isConnected()) break;
|
|
743
|
+
entry.status = "sending";
|
|
744
|
+
this.config.onStatusChange?.();
|
|
745
|
+
try {
|
|
746
|
+
const result = await withRetry(
|
|
747
|
+
entry.sendFn,
|
|
748
|
+
this.config.retryConfig,
|
|
749
|
+
entry.abort.signal
|
|
750
|
+
);
|
|
751
|
+
entry.resolve(result);
|
|
752
|
+
const idx = this.entries.indexOf(entry);
|
|
753
|
+
if (idx !== -1) this.entries.splice(idx, 1);
|
|
754
|
+
this.config.onStatusChange?.();
|
|
755
|
+
this.persist();
|
|
756
|
+
} catch (err) {
|
|
757
|
+
if (entry.abort.signal.aborted) continue;
|
|
758
|
+
entry.status = "failed";
|
|
759
|
+
entry.error = err instanceof Error ? err : new Error(String(err));
|
|
760
|
+
entry.retryCount++;
|
|
761
|
+
this.config.onStatusChange?.();
|
|
762
|
+
this.persist();
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
} finally {
|
|
766
|
+
this.flushing = false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
persist() {
|
|
770
|
+
if (!this.config.storage) return;
|
|
771
|
+
const data = this.entries.map((e) => ({
|
|
772
|
+
optimisticId: e.optimisticId,
|
|
773
|
+
retryCount: e.retryCount
|
|
774
|
+
}));
|
|
775
|
+
const onErr = (err) => {
|
|
776
|
+
this.config.onError?.(
|
|
777
|
+
err instanceof Error ? err : new Error("Queue storage write failed")
|
|
778
|
+
);
|
|
779
|
+
};
|
|
780
|
+
if (data.length === 0) {
|
|
781
|
+
this.config.storage.removeItem(this.storageKey).catch(onErr);
|
|
782
|
+
} else {
|
|
783
|
+
this.config.storage.setItem(this.storageKey, JSON.stringify(data)).catch(onErr);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
295
788
|
// src/use-chat.ts
|
|
296
789
|
var DEFAULT_BACKGROUND_GRACE_MS = 2e3;
|
|
790
|
+
var DEFAULT_MAX_QUEUE_SIZE = 50;
|
|
791
|
+
var queueRegistry = /* @__PURE__ */ new Map();
|
|
297
792
|
function useChat({ config, channelId, profile, onMessage }) {
|
|
298
793
|
const [messages, setMessages] = (0, import_react.useState)([]);
|
|
299
794
|
const [participants, setParticipants] = (0, import_react.useState)([]);
|
|
300
795
|
const [status, setStatus] = (0, import_react.useState)("connecting");
|
|
301
796
|
const [error, setError] = (0, import_react.useState)(null);
|
|
797
|
+
const [pendingMessages, setPendingMessages] = (0, import_react.useState)([]);
|
|
302
798
|
const sessionRef = (0, import_react.useRef)(null);
|
|
303
799
|
const disposedRef = (0, import_react.useRef)(false);
|
|
304
800
|
const messagesRef = (0, import_react.useRef)(messages);
|
|
@@ -315,7 +811,32 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
315
811
|
onMessageRef.current = onMessage;
|
|
316
812
|
const startingRef = (0, import_react.useRef)(false);
|
|
317
813
|
const pendingOptimisticIds = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
814
|
+
const [monitor, setMonitor] = (0, import_react.useState)(null);
|
|
815
|
+
const monitorRef = (0, import_react.useRef)(null);
|
|
816
|
+
monitorRef.current = monitor;
|
|
817
|
+
const queueRef = (0, import_react.useRef)(null);
|
|
818
|
+
const resilienceEnabled = config.resilience !== false;
|
|
819
|
+
const queueEnabled = resilienceEnabled && (typeof config.resilience === "object" ? config.resilience.offlineQueue !== false : true);
|
|
820
|
+
const retryConfig = resolveRetryConfig(config.resilience);
|
|
821
|
+
const maxQueueSize = (resilienceEnabled && config.resilience && typeof config.resilience === "object" ? config.resilience.maxQueueSize : void 0) ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
318
822
|
const backgroundGraceMs = config.backgroundGraceMs ?? (import_react_native.Platform.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS : 0);
|
|
823
|
+
const injectedMonitor = typeof config.resilience === "object" ? config.resilience.networkMonitor : void 0;
|
|
824
|
+
(0, import_react.useEffect)(() => {
|
|
825
|
+
if (!resilienceEnabled) {
|
|
826
|
+
setMonitor(null);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
if (injectedMonitor) {
|
|
830
|
+
setMonitor(injectedMonitor);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const m = createNetworkMonitor();
|
|
834
|
+
setMonitor(m);
|
|
835
|
+
return () => {
|
|
836
|
+
m.dispose();
|
|
837
|
+
setMonitor(null);
|
|
838
|
+
};
|
|
839
|
+
}, [resilienceEnabled, injectedMonitor]);
|
|
319
840
|
const callbacks = {
|
|
320
841
|
onMessage: (message) => {
|
|
321
842
|
if (disposedRef.current) return;
|
|
@@ -340,7 +861,9 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
340
861
|
onStatusChange: (nextStatus) => {
|
|
341
862
|
if (disposedRef.current) return;
|
|
342
863
|
setStatus(nextStatus);
|
|
343
|
-
if (nextStatus === "connected")
|
|
864
|
+
if (nextStatus === "connected") {
|
|
865
|
+
setError(null);
|
|
866
|
+
}
|
|
344
867
|
},
|
|
345
868
|
onError: (err) => {
|
|
346
869
|
if (disposedRef.current) return;
|
|
@@ -360,14 +883,28 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
360
883
|
sessionRef.current = null;
|
|
361
884
|
}
|
|
362
885
|
try {
|
|
363
|
-
const session = await createChatSession(
|
|
886
|
+
const session = await createChatSession(
|
|
887
|
+
configRef.current,
|
|
888
|
+
channelId,
|
|
889
|
+
profileRef.current,
|
|
890
|
+
callbacks,
|
|
891
|
+
monitorRef.current ?? void 0
|
|
892
|
+
);
|
|
364
893
|
if (disposedRef.current) {
|
|
365
894
|
session.disconnect();
|
|
366
895
|
return;
|
|
367
896
|
}
|
|
368
897
|
sessionRef.current = session;
|
|
369
898
|
setParticipants(session.initialParticipants);
|
|
370
|
-
|
|
899
|
+
if (pendingOptimisticIds.current.size > 0) {
|
|
900
|
+
const pendingIds = pendingOptimisticIds.current;
|
|
901
|
+
setMessages((prev) => {
|
|
902
|
+
const pendingMsgs = prev.filter((m) => pendingIds.has(m.id));
|
|
903
|
+
return [...session.initialMessages, ...pendingMsgs];
|
|
904
|
+
});
|
|
905
|
+
} else {
|
|
906
|
+
setMessages(session.initialMessages);
|
|
907
|
+
}
|
|
371
908
|
} catch (err) {
|
|
372
909
|
if (disposedRef.current) return;
|
|
373
910
|
if (err instanceof ChannelClosedError) {
|
|
@@ -383,6 +920,7 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
383
920
|
}
|
|
384
921
|
(0, import_react.useEffect)(() => {
|
|
385
922
|
disposedRef.current = false;
|
|
923
|
+
if (resilienceEnabled && !monitor) return;
|
|
386
924
|
startSession();
|
|
387
925
|
return () => {
|
|
388
926
|
disposedRef.current = true;
|
|
@@ -400,7 +938,52 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
400
938
|
sessionRef.current?.disconnect();
|
|
401
939
|
sessionRef.current = null;
|
|
402
940
|
};
|
|
403
|
-
}, [channelId]);
|
|
941
|
+
}, [channelId, monitor]);
|
|
942
|
+
(0, import_react.useEffect)(() => {
|
|
943
|
+
if (!queueEnabled || !monitor || !retryConfig) return;
|
|
944
|
+
let entry = queueRegistry.get(channelId);
|
|
945
|
+
if (!entry) {
|
|
946
|
+
const queue = new MessageQueue({
|
|
947
|
+
channelId,
|
|
948
|
+
maxSize: maxQueueSize,
|
|
949
|
+
retryConfig,
|
|
950
|
+
networkMonitor: monitor,
|
|
951
|
+
storage: typeof config.resilience === "object" ? config.resilience.queueStorage : void 0,
|
|
952
|
+
onError: (err) => {
|
|
953
|
+
if (!disposedRef.current) setError(err);
|
|
954
|
+
},
|
|
955
|
+
onStatusChange: () => {
|
|
956
|
+
if (!disposedRef.current) {
|
|
957
|
+
setPendingMessages(queueRef.current?.getAll() ?? []);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
entry = { queue, refCount: 0 };
|
|
962
|
+
queueRegistry.set(channelId, entry);
|
|
963
|
+
}
|
|
964
|
+
entry.refCount++;
|
|
965
|
+
queueRef.current = entry.queue;
|
|
966
|
+
return () => {
|
|
967
|
+
const e = queueRegistry.get(channelId);
|
|
968
|
+
if (e) {
|
|
969
|
+
e.refCount--;
|
|
970
|
+
if (e.refCount <= 0) {
|
|
971
|
+
e.queue.dispose();
|
|
972
|
+
queueRegistry.delete(channelId);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
queueRef.current = null;
|
|
976
|
+
};
|
|
977
|
+
}, [channelId, queueEnabled, monitor]);
|
|
978
|
+
(0, import_react.useEffect)(() => {
|
|
979
|
+
if (!monitor || !resilienceEnabled) return;
|
|
980
|
+
const unsub = monitor.subscribe((connected) => {
|
|
981
|
+
if (connected && statusRef.current === "error" && !startingRef.current) {
|
|
982
|
+
startSession();
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
return unsub;
|
|
986
|
+
}, [channelId, resilienceEnabled, monitor]);
|
|
404
987
|
(0, import_react.useEffect)(() => {
|
|
405
988
|
function teardownSession() {
|
|
406
989
|
sessionRef.current?.disconnect();
|
|
@@ -446,12 +1029,11 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
446
1029
|
const session = sessionRef.current;
|
|
447
1030
|
if (!session) throw new ChatDisconnectedError(statusRef.current);
|
|
448
1031
|
const optimistic = configRef.current.optimisticSend !== false;
|
|
449
|
-
|
|
1032
|
+
const messageKey = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
450
1033
|
if (optimistic) {
|
|
451
|
-
|
|
452
|
-
pendingOptimisticIds.current.add(optimisticId);
|
|
1034
|
+
pendingOptimisticIds.current.add(messageKey);
|
|
453
1035
|
const provisionalMsg = {
|
|
454
|
-
id:
|
|
1036
|
+
id: messageKey,
|
|
455
1037
|
channel_id: channelId,
|
|
456
1038
|
sender_id: profileRef.current.id,
|
|
457
1039
|
sender_role: profileRef.current.role,
|
|
@@ -462,35 +1044,91 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
462
1044
|
};
|
|
463
1045
|
setMessages((prev) => [...prev, provisionalMsg]);
|
|
464
1046
|
}
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
|
|
1047
|
+
const doSend = () => {
|
|
1048
|
+
const s = sessionRef.current;
|
|
1049
|
+
if (!s) throw new ChatDisconnectedError(statusRef.current);
|
|
1050
|
+
return s.sendMessage(type, body, attributes, messageKey);
|
|
1051
|
+
};
|
|
1052
|
+
const handleSuccess = (response) => {
|
|
1053
|
+
if (optimistic) {
|
|
1054
|
+
pendingOptimisticIds.current.delete(messageKey);
|
|
469
1055
|
setMessages((prev) => {
|
|
470
|
-
const stillPending = prev.some((m) => m.id ===
|
|
1056
|
+
const stillPending = prev.some((m) => m.id === messageKey);
|
|
471
1057
|
if (!stillPending) return prev;
|
|
472
1058
|
return prev.map(
|
|
473
|
-
(m) => m.id ===
|
|
1059
|
+
(m) => m.id === messageKey ? { ...m, id: response.id, created_at: response.created_at } : m
|
|
474
1060
|
);
|
|
475
1061
|
});
|
|
476
1062
|
}
|
|
1063
|
+
};
|
|
1064
|
+
const handleError = (_err) => {
|
|
1065
|
+
if (optimistic) {
|
|
1066
|
+
pendingOptimisticIds.current.delete(messageKey);
|
|
1067
|
+
setMessages((prev) => prev.filter((m) => m.id !== messageKey));
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
if (queueRef.current) {
|
|
1071
|
+
try {
|
|
1072
|
+
const response = await queueRef.current.enqueue(doSend, messageKey);
|
|
1073
|
+
handleSuccess(response);
|
|
1074
|
+
return response;
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
if (err instanceof QueueFullError) {
|
|
1077
|
+
handleError(err);
|
|
1078
|
+
throw err;
|
|
1079
|
+
}
|
|
1080
|
+
if (err instanceof RetryExhaustedError || !isRetryableError(err)) {
|
|
1081
|
+
if (optimistic && err instanceof RetryExhaustedError) {
|
|
1082
|
+
} else {
|
|
1083
|
+
handleError(err);
|
|
1084
|
+
}
|
|
1085
|
+
throw err;
|
|
1086
|
+
}
|
|
1087
|
+
handleError(err);
|
|
1088
|
+
throw err;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
const response = await doSend();
|
|
1093
|
+
handleSuccess(response);
|
|
477
1094
|
return response;
|
|
478
1095
|
} catch (err) {
|
|
479
|
-
|
|
480
|
-
pendingOptimisticIds.current.delete(optimisticId);
|
|
481
|
-
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
482
|
-
}
|
|
1096
|
+
handleError(err);
|
|
483
1097
|
throw err;
|
|
484
1098
|
}
|
|
485
1099
|
},
|
|
486
1100
|
[channelId]
|
|
487
1101
|
);
|
|
1102
|
+
const cancelMessage = (0, import_react.useCallback)(
|
|
1103
|
+
(optimisticId) => {
|
|
1104
|
+
queueRef.current?.cancel(optimisticId);
|
|
1105
|
+
pendingOptimisticIds.current.delete(optimisticId);
|
|
1106
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
1107
|
+
},
|
|
1108
|
+
[]
|
|
1109
|
+
);
|
|
1110
|
+
const retryMessage = (0, import_react.useCallback)(
|
|
1111
|
+
(optimisticId) => {
|
|
1112
|
+
queueRef.current?.retry(optimisticId);
|
|
1113
|
+
},
|
|
1114
|
+
[]
|
|
1115
|
+
);
|
|
488
1116
|
const disconnect = (0, import_react.useCallback)(() => {
|
|
489
1117
|
sessionRef.current?.disconnect();
|
|
490
1118
|
sessionRef.current = null;
|
|
491
1119
|
setStatus("disconnected");
|
|
492
1120
|
}, []);
|
|
493
|
-
return {
|
|
1121
|
+
return {
|
|
1122
|
+
messages,
|
|
1123
|
+
participants,
|
|
1124
|
+
status,
|
|
1125
|
+
error,
|
|
1126
|
+
sendMessage,
|
|
1127
|
+
disconnect,
|
|
1128
|
+
pendingMessages,
|
|
1129
|
+
cancelMessage,
|
|
1130
|
+
retryMessage
|
|
1131
|
+
};
|
|
494
1132
|
}
|
|
495
1133
|
|
|
496
1134
|
// src/use-unread.ts
|
|
@@ -509,6 +1147,25 @@ function useUnread(options) {
|
|
|
509
1147
|
const appStateRef = (0, import_react2.useRef)(import_react_native2.AppState.currentState);
|
|
510
1148
|
const backgroundTimerRef = (0, import_react2.useRef)(null);
|
|
511
1149
|
const backgroundGraceMs = config.backgroundGraceMs ?? (import_react_native2.Platform.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS2 : 0);
|
|
1150
|
+
const [monitor, setMonitor] = (0, import_react2.useState)(null);
|
|
1151
|
+
const resilienceEnabled = config.resilience !== false;
|
|
1152
|
+
const injectedMonitor = typeof config.resilience === "object" ? config.resilience.networkMonitor : void 0;
|
|
1153
|
+
(0, import_react2.useEffect)(() => {
|
|
1154
|
+
if (!resilienceEnabled) {
|
|
1155
|
+
setMonitor(null);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (injectedMonitor) {
|
|
1159
|
+
setMonitor(injectedMonitor);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const m = createNetworkMonitor();
|
|
1163
|
+
setMonitor(m);
|
|
1164
|
+
return () => {
|
|
1165
|
+
m.dispose();
|
|
1166
|
+
setMonitor(null);
|
|
1167
|
+
};
|
|
1168
|
+
}, [resilienceEnabled, injectedMonitor]);
|
|
512
1169
|
const connect = (0, import_react2.useCallback)(() => {
|
|
513
1170
|
connRef.current?.close();
|
|
514
1171
|
connRef.current = null;
|
|
@@ -520,7 +1177,8 @@ function useUnread(options) {
|
|
|
520
1177
|
url,
|
|
521
1178
|
headers: customHeaders,
|
|
522
1179
|
reconnectDelayMs: configRef.current.reconnectDelayMs,
|
|
523
|
-
customEvents: UNREAD_CUSTOM_EVENTS
|
|
1180
|
+
customEvents: UNREAD_CUSTOM_EVENTS,
|
|
1181
|
+
networkMonitor: monitor ?? void 0
|
|
524
1182
|
},
|
|
525
1183
|
{
|
|
526
1184
|
onOpen: () => {
|
|
@@ -552,7 +1210,7 @@ function useUnread(options) {
|
|
|
552
1210
|
}
|
|
553
1211
|
}
|
|
554
1212
|
);
|
|
555
|
-
}, [channelId, participantId]);
|
|
1213
|
+
}, [channelId, participantId, monitor]);
|
|
556
1214
|
const disconnect = (0, import_react2.useCallback)(() => {
|
|
557
1215
|
connRef.current?.close();
|
|
558
1216
|
connRef.current = null;
|
|
@@ -565,6 +1223,7 @@ function useUnread(options) {
|
|
|
565
1223
|
disconnect();
|
|
566
1224
|
return;
|
|
567
1225
|
}
|
|
1226
|
+
if (resilienceEnabled && !monitor) return;
|
|
568
1227
|
connect();
|
|
569
1228
|
return () => {
|
|
570
1229
|
disconnect();
|
|
@@ -616,11 +1275,21 @@ function useUnread(options) {
|
|
|
616
1275
|
0 && (module.exports = {
|
|
617
1276
|
ChannelClosedError,
|
|
618
1277
|
ChatDisconnectedError,
|
|
1278
|
+
HttpError,
|
|
1279
|
+
QueueFullError,
|
|
1280
|
+
RetryExhaustedError,
|
|
1281
|
+
calculateBackoff,
|
|
1282
|
+
createAsyncStorageAdapter,
|
|
619
1283
|
createChatSession,
|
|
620
1284
|
createManifest,
|
|
1285
|
+
createNetworkMonitor,
|
|
1286
|
+
createQueueStorage,
|
|
621
1287
|
createSSEConnection,
|
|
1288
|
+
isRetryableError,
|
|
1289
|
+
resolveRetryConfig,
|
|
622
1290
|
resolveServerUrl,
|
|
623
1291
|
useChat,
|
|
624
|
-
useUnread
|
|
1292
|
+
useUnread,
|
|
1293
|
+
withRetry
|
|
625
1294
|
});
|
|
626
1295
|
//# sourceMappingURL=index.js.map
|