@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.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,342 @@ 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: "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
|
+
/**
|
|
735
|
+
* Trigger a flush of queued messages. Returns immediately if a flush is
|
|
736
|
+
* already in progress or the network is offline. The in-progress flush
|
|
737
|
+
* will pick up any newly queued entries via its while loop.
|
|
738
|
+
*/
|
|
739
|
+
async flush() {
|
|
740
|
+
if (this.flushing) return;
|
|
741
|
+
if (!this.config.networkMonitor.isConnected()) return;
|
|
742
|
+
this.flushing = true;
|
|
743
|
+
let awaitingSession = false;
|
|
744
|
+
try {
|
|
745
|
+
while (this.entries.length > 0) {
|
|
746
|
+
const entry = this.entries.find((e) => e.status === "queued");
|
|
747
|
+
if (!entry) break;
|
|
748
|
+
if (!this.config.networkMonitor.isConnected()) break;
|
|
749
|
+
entry.status = "sending";
|
|
750
|
+
this.config.onStatusChange?.();
|
|
751
|
+
try {
|
|
752
|
+
const result = await withRetry(
|
|
753
|
+
entry.sendFn,
|
|
754
|
+
this.config.retryConfig,
|
|
755
|
+
entry.abort.signal
|
|
756
|
+
);
|
|
757
|
+
entry.resolve(result);
|
|
758
|
+
const idx = this.entries.indexOf(entry);
|
|
759
|
+
if (idx !== -1) this.entries.splice(idx, 1);
|
|
760
|
+
this.config.onStatusChange?.();
|
|
761
|
+
this.persist();
|
|
762
|
+
} catch (err) {
|
|
763
|
+
if (entry.abort.signal.aborted) continue;
|
|
764
|
+
if (err instanceof ChatDisconnectedError) {
|
|
765
|
+
entry.status = "queued";
|
|
766
|
+
this.config.onStatusChange?.();
|
|
767
|
+
awaitingSession = true;
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
entry.status = "failed";
|
|
771
|
+
entry.error = err instanceof Error ? err : new Error(String(err));
|
|
772
|
+
entry.retryCount++;
|
|
773
|
+
this.config.onStatusChange?.();
|
|
774
|
+
this.persist();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
} finally {
|
|
778
|
+
this.flushing = false;
|
|
779
|
+
if (!awaitingSession && this.entries.some((e) => e.status === "queued") && this.config.networkMonitor.isConnected()) {
|
|
780
|
+
queueMicrotask(() => this.flush());
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
persist() {
|
|
785
|
+
if (!this.config.storage) return;
|
|
786
|
+
const data = this.entries.map((e) => ({
|
|
787
|
+
optimisticId: e.optimisticId,
|
|
788
|
+
retryCount: e.retryCount
|
|
789
|
+
}));
|
|
790
|
+
const onErr = (err) => {
|
|
791
|
+
this.config.onError?.(
|
|
792
|
+
err instanceof Error ? err : new Error("Queue storage write failed")
|
|
793
|
+
);
|
|
794
|
+
};
|
|
795
|
+
if (data.length === 0) {
|
|
796
|
+
this.config.storage.removeItem(this.storageKey).catch(onErr);
|
|
797
|
+
} else {
|
|
798
|
+
this.config.storage.setItem(this.storageKey, JSON.stringify(data)).catch(onErr);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
|
|
295
803
|
// src/use-chat.ts
|
|
296
804
|
var DEFAULT_BACKGROUND_GRACE_MS = 2e3;
|
|
805
|
+
var DEFAULT_MAX_QUEUE_SIZE = 50;
|
|
806
|
+
var queueRegistry = /* @__PURE__ */ new Map();
|
|
297
807
|
function useChat({ config, channelId, profile, onMessage }) {
|
|
298
808
|
const [messages, setMessages] = (0, import_react.useState)([]);
|
|
299
809
|
const [participants, setParticipants] = (0, import_react.useState)([]);
|
|
300
810
|
const [status, setStatus] = (0, import_react.useState)("connecting");
|
|
301
811
|
const [error, setError] = (0, import_react.useState)(null);
|
|
812
|
+
const [pendingMessages, setPendingMessages] = (0, import_react.useState)([]);
|
|
302
813
|
const sessionRef = (0, import_react.useRef)(null);
|
|
303
814
|
const disposedRef = (0, import_react.useRef)(false);
|
|
304
815
|
const messagesRef = (0, import_react.useRef)(messages);
|
|
@@ -315,10 +826,36 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
315
826
|
onMessageRef.current = onMessage;
|
|
316
827
|
const startingRef = (0, import_react.useRef)(false);
|
|
317
828
|
const pendingOptimisticIds = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
829
|
+
const [monitor, setMonitor] = (0, import_react.useState)(null);
|
|
830
|
+
const monitorRef = (0, import_react.useRef)(null);
|
|
831
|
+
monitorRef.current = monitor;
|
|
832
|
+
const queueRef = (0, import_react.useRef)(null);
|
|
833
|
+
const resilienceEnabled = config.resilience !== false;
|
|
834
|
+
const queueEnabled = resilienceEnabled && (typeof config.resilience === "object" ? config.resilience.offlineQueue !== false : true);
|
|
835
|
+
const retryConfig = resolveRetryConfig(config.resilience);
|
|
836
|
+
const maxQueueSize = (resilienceEnabled && config.resilience && typeof config.resilience === "object" ? config.resilience.maxQueueSize : void 0) ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
318
837
|
const backgroundGraceMs = config.backgroundGraceMs ?? (import_react_native.Platform.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS : 0);
|
|
838
|
+
const injectedMonitor = typeof config.resilience === "object" ? config.resilience.networkMonitor : void 0;
|
|
839
|
+
(0, import_react.useEffect)(() => {
|
|
840
|
+
if (!resilienceEnabled) {
|
|
841
|
+
setMonitor(null);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (injectedMonitor) {
|
|
845
|
+
setMonitor(injectedMonitor);
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const m = createNetworkMonitor();
|
|
849
|
+
setMonitor(m);
|
|
850
|
+
return () => {
|
|
851
|
+
m.dispose();
|
|
852
|
+
setMonitor(null);
|
|
853
|
+
};
|
|
854
|
+
}, [resilienceEnabled, injectedMonitor]);
|
|
319
855
|
const callbacks = {
|
|
320
856
|
onMessage: (message) => {
|
|
321
857
|
if (disposedRef.current) return;
|
|
858
|
+
let matchedOptimisticId = null;
|
|
322
859
|
setMessages((prev) => {
|
|
323
860
|
if (pendingOptimisticIds.current.size === 0) {
|
|
324
861
|
return [...prev, message];
|
|
@@ -327,20 +864,28 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
327
864
|
(m) => pendingOptimisticIds.current.has(m.id) && m.sender_id === message.sender_id && m.body === message.body && m.type === message.type
|
|
328
865
|
);
|
|
329
866
|
if (optimisticIdx !== -1) {
|
|
330
|
-
|
|
331
|
-
pendingOptimisticIds.current.delete(
|
|
867
|
+
matchedOptimisticId = prev[optimisticIdx].id;
|
|
868
|
+
pendingOptimisticIds.current.delete(matchedOptimisticId);
|
|
332
869
|
const next = [...prev];
|
|
333
870
|
next[optimisticIdx] = message;
|
|
334
871
|
return next;
|
|
335
872
|
}
|
|
336
873
|
return [...prev, message];
|
|
337
874
|
});
|
|
875
|
+
if (matchedOptimisticId) {
|
|
876
|
+
const s = queueRef.current?.getStatus(matchedOptimisticId);
|
|
877
|
+
if (s && s.status !== "sending") {
|
|
878
|
+
queueRef.current?.cancel(matchedOptimisticId);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
338
881
|
onMessageRef.current?.(message);
|
|
339
882
|
},
|
|
340
883
|
onStatusChange: (nextStatus) => {
|
|
341
884
|
if (disposedRef.current) return;
|
|
342
885
|
setStatus(nextStatus);
|
|
343
|
-
if (nextStatus === "connected")
|
|
886
|
+
if (nextStatus === "connected") {
|
|
887
|
+
setError(null);
|
|
888
|
+
}
|
|
344
889
|
},
|
|
345
890
|
onError: (err) => {
|
|
346
891
|
if (disposedRef.current) return;
|
|
@@ -360,14 +905,29 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
360
905
|
sessionRef.current = null;
|
|
361
906
|
}
|
|
362
907
|
try {
|
|
363
|
-
const session = await createChatSession(
|
|
908
|
+
const session = await createChatSession(
|
|
909
|
+
configRef.current,
|
|
910
|
+
channelId,
|
|
911
|
+
profileRef.current,
|
|
912
|
+
callbacks,
|
|
913
|
+
monitorRef.current ?? void 0
|
|
914
|
+
);
|
|
364
915
|
if (disposedRef.current) {
|
|
365
916
|
session.disconnect();
|
|
366
917
|
return;
|
|
367
918
|
}
|
|
368
919
|
sessionRef.current = session;
|
|
369
920
|
setParticipants(session.initialParticipants);
|
|
370
|
-
|
|
921
|
+
queueRef.current?.flush();
|
|
922
|
+
if (pendingOptimisticIds.current.size > 0) {
|
|
923
|
+
const pendingIds = pendingOptimisticIds.current;
|
|
924
|
+
setMessages((prev) => {
|
|
925
|
+
const pendingMsgs = prev.filter((m) => pendingIds.has(m.id));
|
|
926
|
+
return [...session.initialMessages, ...pendingMsgs];
|
|
927
|
+
});
|
|
928
|
+
} else {
|
|
929
|
+
setMessages(session.initialMessages);
|
|
930
|
+
}
|
|
371
931
|
} catch (err) {
|
|
372
932
|
if (disposedRef.current) return;
|
|
373
933
|
if (err instanceof ChannelClosedError) {
|
|
@@ -383,6 +943,7 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
383
943
|
}
|
|
384
944
|
(0, import_react.useEffect)(() => {
|
|
385
945
|
disposedRef.current = false;
|
|
946
|
+
if (resilienceEnabled && !monitor) return;
|
|
386
947
|
startSession();
|
|
387
948
|
return () => {
|
|
388
949
|
disposedRef.current = true;
|
|
@@ -400,7 +961,52 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
400
961
|
sessionRef.current?.disconnect();
|
|
401
962
|
sessionRef.current = null;
|
|
402
963
|
};
|
|
403
|
-
}, [channelId]);
|
|
964
|
+
}, [channelId, monitor]);
|
|
965
|
+
(0, import_react.useEffect)(() => {
|
|
966
|
+
if (!queueEnabled || !monitor || !retryConfig) return;
|
|
967
|
+
let entry = queueRegistry.get(channelId);
|
|
968
|
+
if (!entry) {
|
|
969
|
+
const queue = new MessageQueue({
|
|
970
|
+
channelId,
|
|
971
|
+
maxSize: maxQueueSize,
|
|
972
|
+
retryConfig,
|
|
973
|
+
networkMonitor: monitor,
|
|
974
|
+
storage: typeof config.resilience === "object" ? config.resilience.queueStorage : void 0,
|
|
975
|
+
onError: (err) => {
|
|
976
|
+
if (!disposedRef.current) setError(err);
|
|
977
|
+
},
|
|
978
|
+
onStatusChange: () => {
|
|
979
|
+
if (!disposedRef.current) {
|
|
980
|
+
setPendingMessages(queueRef.current?.getAll() ?? []);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
entry = { queue, refCount: 0 };
|
|
985
|
+
queueRegistry.set(channelId, entry);
|
|
986
|
+
}
|
|
987
|
+
entry.refCount++;
|
|
988
|
+
queueRef.current = entry.queue;
|
|
989
|
+
return () => {
|
|
990
|
+
const e = queueRegistry.get(channelId);
|
|
991
|
+
if (e) {
|
|
992
|
+
e.refCount--;
|
|
993
|
+
if (e.refCount <= 0) {
|
|
994
|
+
e.queue.dispose();
|
|
995
|
+
queueRegistry.delete(channelId);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
queueRef.current = null;
|
|
999
|
+
};
|
|
1000
|
+
}, [channelId, queueEnabled, monitor]);
|
|
1001
|
+
(0, import_react.useEffect)(() => {
|
|
1002
|
+
if (!monitor || !resilienceEnabled) return;
|
|
1003
|
+
const unsub = monitor.subscribe((connected) => {
|
|
1004
|
+
if (connected && statusRef.current === "error" && !startingRef.current) {
|
|
1005
|
+
startSession();
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
return unsub;
|
|
1009
|
+
}, [channelId, resilienceEnabled, monitor]);
|
|
404
1010
|
(0, import_react.useEffect)(() => {
|
|
405
1011
|
function teardownSession() {
|
|
406
1012
|
sessionRef.current?.disconnect();
|
|
@@ -446,12 +1052,11 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
446
1052
|
const session = sessionRef.current;
|
|
447
1053
|
if (!session) throw new ChatDisconnectedError(statusRef.current);
|
|
448
1054
|
const optimistic = configRef.current.optimisticSend !== false;
|
|
449
|
-
|
|
1055
|
+
const messageKey = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
450
1056
|
if (optimistic) {
|
|
451
|
-
|
|
452
|
-
pendingOptimisticIds.current.add(optimisticId);
|
|
1057
|
+
pendingOptimisticIds.current.add(messageKey);
|
|
453
1058
|
const provisionalMsg = {
|
|
454
|
-
id:
|
|
1059
|
+
id: messageKey,
|
|
455
1060
|
channel_id: channelId,
|
|
456
1061
|
sender_id: profileRef.current.id,
|
|
457
1062
|
sender_role: profileRef.current.role,
|
|
@@ -462,35 +1067,91 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
462
1067
|
};
|
|
463
1068
|
setMessages((prev) => [...prev, provisionalMsg]);
|
|
464
1069
|
}
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
|
|
1070
|
+
const doSend = () => {
|
|
1071
|
+
const s = sessionRef.current;
|
|
1072
|
+
if (!s) throw new ChatDisconnectedError(statusRef.current);
|
|
1073
|
+
return s.sendMessage(type, body, attributes, messageKey);
|
|
1074
|
+
};
|
|
1075
|
+
const handleSuccess = (response) => {
|
|
1076
|
+
if (optimistic) {
|
|
1077
|
+
pendingOptimisticIds.current.delete(messageKey);
|
|
469
1078
|
setMessages((prev) => {
|
|
470
|
-
const stillPending = prev.some((m) => m.id ===
|
|
1079
|
+
const stillPending = prev.some((m) => m.id === messageKey);
|
|
471
1080
|
if (!stillPending) return prev;
|
|
472
1081
|
return prev.map(
|
|
473
|
-
(m) => m.id ===
|
|
1082
|
+
(m) => m.id === messageKey ? { ...m, id: response.id, created_at: response.created_at } : m
|
|
474
1083
|
);
|
|
475
1084
|
});
|
|
476
1085
|
}
|
|
1086
|
+
};
|
|
1087
|
+
const handleError = (_err) => {
|
|
1088
|
+
if (optimistic) {
|
|
1089
|
+
pendingOptimisticIds.current.delete(messageKey);
|
|
1090
|
+
setMessages((prev) => prev.filter((m) => m.id !== messageKey));
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
if (queueRef.current) {
|
|
1094
|
+
try {
|
|
1095
|
+
const response = await queueRef.current.enqueue(doSend, messageKey);
|
|
1096
|
+
handleSuccess(response);
|
|
1097
|
+
return response;
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
if (err instanceof QueueFullError) {
|
|
1100
|
+
handleError(err);
|
|
1101
|
+
throw err;
|
|
1102
|
+
}
|
|
1103
|
+
if (err instanceof RetryExhaustedError || !isRetryableError(err)) {
|
|
1104
|
+
if (optimistic && err instanceof RetryExhaustedError) {
|
|
1105
|
+
} else {
|
|
1106
|
+
handleError(err);
|
|
1107
|
+
}
|
|
1108
|
+
throw err;
|
|
1109
|
+
}
|
|
1110
|
+
handleError(err);
|
|
1111
|
+
throw err;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
const response = await doSend();
|
|
1116
|
+
handleSuccess(response);
|
|
477
1117
|
return response;
|
|
478
1118
|
} catch (err) {
|
|
479
|
-
|
|
480
|
-
pendingOptimisticIds.current.delete(optimisticId);
|
|
481
|
-
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
482
|
-
}
|
|
1119
|
+
handleError(err);
|
|
483
1120
|
throw err;
|
|
484
1121
|
}
|
|
485
1122
|
},
|
|
486
1123
|
[channelId]
|
|
487
1124
|
);
|
|
1125
|
+
const cancelMessage = (0, import_react.useCallback)(
|
|
1126
|
+
(optimisticId) => {
|
|
1127
|
+
queueRef.current?.cancel(optimisticId);
|
|
1128
|
+
pendingOptimisticIds.current.delete(optimisticId);
|
|
1129
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
1130
|
+
},
|
|
1131
|
+
[]
|
|
1132
|
+
);
|
|
1133
|
+
const retryMessage = (0, import_react.useCallback)(
|
|
1134
|
+
(optimisticId) => {
|
|
1135
|
+
queueRef.current?.retry(optimisticId);
|
|
1136
|
+
},
|
|
1137
|
+
[]
|
|
1138
|
+
);
|
|
488
1139
|
const disconnect = (0, import_react.useCallback)(() => {
|
|
489
1140
|
sessionRef.current?.disconnect();
|
|
490
1141
|
sessionRef.current = null;
|
|
491
1142
|
setStatus("disconnected");
|
|
492
1143
|
}, []);
|
|
493
|
-
return {
|
|
1144
|
+
return {
|
|
1145
|
+
messages,
|
|
1146
|
+
participants,
|
|
1147
|
+
status,
|
|
1148
|
+
error,
|
|
1149
|
+
sendMessage,
|
|
1150
|
+
disconnect,
|
|
1151
|
+
pendingMessages,
|
|
1152
|
+
cancelMessage,
|
|
1153
|
+
retryMessage
|
|
1154
|
+
};
|
|
494
1155
|
}
|
|
495
1156
|
|
|
496
1157
|
// src/use-unread.ts
|
|
@@ -509,6 +1170,25 @@ function useUnread(options) {
|
|
|
509
1170
|
const appStateRef = (0, import_react2.useRef)(import_react_native2.AppState.currentState);
|
|
510
1171
|
const backgroundTimerRef = (0, import_react2.useRef)(null);
|
|
511
1172
|
const backgroundGraceMs = config.backgroundGraceMs ?? (import_react_native2.Platform.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS2 : 0);
|
|
1173
|
+
const [monitor, setMonitor] = (0, import_react2.useState)(null);
|
|
1174
|
+
const resilienceEnabled = config.resilience !== false;
|
|
1175
|
+
const injectedMonitor = typeof config.resilience === "object" ? config.resilience.networkMonitor : void 0;
|
|
1176
|
+
(0, import_react2.useEffect)(() => {
|
|
1177
|
+
if (!resilienceEnabled) {
|
|
1178
|
+
setMonitor(null);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
if (injectedMonitor) {
|
|
1182
|
+
setMonitor(injectedMonitor);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
const m = createNetworkMonitor();
|
|
1186
|
+
setMonitor(m);
|
|
1187
|
+
return () => {
|
|
1188
|
+
m.dispose();
|
|
1189
|
+
setMonitor(null);
|
|
1190
|
+
};
|
|
1191
|
+
}, [resilienceEnabled, injectedMonitor]);
|
|
512
1192
|
const connect = (0, import_react2.useCallback)(() => {
|
|
513
1193
|
connRef.current?.close();
|
|
514
1194
|
connRef.current = null;
|
|
@@ -520,7 +1200,8 @@ function useUnread(options) {
|
|
|
520
1200
|
url,
|
|
521
1201
|
headers: customHeaders,
|
|
522
1202
|
reconnectDelayMs: configRef.current.reconnectDelayMs,
|
|
523
|
-
customEvents: UNREAD_CUSTOM_EVENTS
|
|
1203
|
+
customEvents: UNREAD_CUSTOM_EVENTS,
|
|
1204
|
+
networkMonitor: monitor ?? void 0
|
|
524
1205
|
},
|
|
525
1206
|
{
|
|
526
1207
|
onOpen: () => {
|
|
@@ -552,7 +1233,7 @@ function useUnread(options) {
|
|
|
552
1233
|
}
|
|
553
1234
|
}
|
|
554
1235
|
);
|
|
555
|
-
}, [channelId, participantId]);
|
|
1236
|
+
}, [channelId, participantId, monitor]);
|
|
556
1237
|
const disconnect = (0, import_react2.useCallback)(() => {
|
|
557
1238
|
connRef.current?.close();
|
|
558
1239
|
connRef.current = null;
|
|
@@ -565,6 +1246,7 @@ function useUnread(options) {
|
|
|
565
1246
|
disconnect();
|
|
566
1247
|
return;
|
|
567
1248
|
}
|
|
1249
|
+
if (resilienceEnabled && !monitor) return;
|
|
568
1250
|
connect();
|
|
569
1251
|
return () => {
|
|
570
1252
|
disconnect();
|
|
@@ -616,11 +1298,21 @@ function useUnread(options) {
|
|
|
616
1298
|
0 && (module.exports = {
|
|
617
1299
|
ChannelClosedError,
|
|
618
1300
|
ChatDisconnectedError,
|
|
1301
|
+
HttpError,
|
|
1302
|
+
QueueFullError,
|
|
1303
|
+
RetryExhaustedError,
|
|
1304
|
+
calculateBackoff,
|
|
1305
|
+
createAsyncStorageAdapter,
|
|
619
1306
|
createChatSession,
|
|
620
1307
|
createManifest,
|
|
1308
|
+
createNetworkMonitor,
|
|
1309
|
+
createQueueStorage,
|
|
621
1310
|
createSSEConnection,
|
|
1311
|
+
isRetryableError,
|
|
1312
|
+
resolveRetryConfig,
|
|
622
1313
|
resolveServerUrl,
|
|
623
1314
|
useChat,
|
|
624
|
-
useUnread
|
|
1315
|
+
useUnread,
|
|
1316
|
+
withRetry
|
|
625
1317
|
});
|
|
626
1318
|
//# sourceMappingURL=index.js.map
|