@pedi/chika-sdk 1.0.4 → 1.0.5
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 +26 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ Provides a drop-in React hook (`useChat`) that connects your React Native app to
|
|
|
19
19
|
|
|
20
20
|
- `useChat<D>()` React hook with full TypeScript generics
|
|
21
21
|
- `createChatSession<D>()` imperative API for non-React usage
|
|
22
|
+
- **`useUnread()` hook** — Real-time unread count tracking via dedicated SSE stream with passive listening support
|
|
22
23
|
- Automatic SSE reconnection with configurable delay
|
|
23
24
|
- Platform-aware AppState handling (iOS vs Android)
|
|
24
25
|
- Optimistic message sending with deduplication
|
|
@@ -45,6 +46,31 @@ function ChatScreen({ bookingId, user }) {
|
|
|
45
46
|
}
|
|
46
47
|
```
|
|
47
48
|
|
|
49
|
+
### Unread Notifications
|
|
50
|
+
|
|
51
|
+
Monitor unread message counts in real-time — even for channels the user hasn't joined yet:
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { useUnread } from '@pedi/chika-sdk';
|
|
55
|
+
|
|
56
|
+
function ChatListItem({ channelId, userId, config }) {
|
|
57
|
+
const { unreadCount, hasUnread, lastMessageAt } = useUnread({
|
|
58
|
+
config,
|
|
59
|
+
channelId,
|
|
60
|
+
participantId: userId,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View>
|
|
65
|
+
<Text>{channelId}</Text>
|
|
66
|
+
{hasUnread && <Badge count={unreadCount} />}
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The hook handles SSE reconnection and AppState-aware lifecycle management automatically. Disable it with `enabled: false` when `useChat` is already active on the same channel.
|
|
73
|
+
|
|
48
74
|
**Peer dependencies:** `react >= 18`, `react-native >= 0.72`
|
|
49
75
|
|
|
50
76
|
## Documentation
|
package/dist/index.js
CHANGED
|
@@ -181,8 +181,9 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
181
181
|
const seenMessageIds = new Set(messages.map((m) => m.id));
|
|
182
182
|
let sseConn = null;
|
|
183
183
|
let disposed = false;
|
|
184
|
+
const TRIM_THRESHOLD = MAX_SEEN_IDS * 1.5;
|
|
184
185
|
function trimSeenIds() {
|
|
185
|
-
if (seenMessageIds.size <=
|
|
186
|
+
if (seenMessageIds.size <= TRIM_THRESHOLD) return;
|
|
186
187
|
const ids = [...seenMessageIds];
|
|
187
188
|
seenMessageIds.clear();
|
|
188
189
|
for (const id of ids.slice(-MAX_SEEN_IDS)) {
|
|
@@ -319,6 +320,9 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
319
320
|
onMessage: (message) => {
|
|
320
321
|
if (disposedRef.current) return;
|
|
321
322
|
setMessages((prev) => {
|
|
323
|
+
if (pendingOptimisticIds.current.size === 0) {
|
|
324
|
+
return [...prev, message];
|
|
325
|
+
}
|
|
322
326
|
const optimisticIdx = prev.findIndex(
|
|
323
327
|
(m) => pendingOptimisticIds.current.has(m.id) && m.sender_id === message.sender_id && m.body === message.body && m.type === message.type
|
|
324
328
|
);
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/use-chat.ts","../src/errors.ts","../src/resolve-url.ts","../src/sse-connection.ts","../src/session.ts","../src/use-unread.ts"],"sourcesContent":["export { useChat } from './use-chat';\nexport { useUnread } from './use-unread';\nexport { createChatSession } from './session';\nexport { resolveServerUrl, createManifest } from './resolve-url';\nexport { createSSEConnection } from './sse-connection';\nexport { ChatDisconnectedError, ChannelClosedError } from './errors';\n\nexport type { ChatConfig, ChatStatus, UseChatOptions, UseChatReturn } from './types';\nexport type { ChatSession, SessionCallbacks } from './session';\nexport type { UseUnreadOptions, UseUnreadReturn } from './use-unread';\nexport type { SSEConnection, SSEConnectionConfig, SSEConnectionCallbacks } from './sse-connection';\n\nexport type {\n ChatDomain,\n DefaultDomain,\n Message,\n Participant,\n MessageAttributes,\n SendMessageResponse,\n ChatManifest,\n ChatBucket,\n UnreadCountResponse,\n MarkReadRequest,\n SSEUnreadUpdateEvent,\n SSEUnreadClearEvent,\n SSEUnreadEvent,\n PediChat,\n PediRole,\n PediVehicle,\n PediLocation,\n PediParticipantMeta,\n PediMessageType,\n PediMessageAttributes,\n} from '@pedi/chika-types';\n","import { useEffect, useRef, useState, useCallback } from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport type {\n ChatDomain,\n DefaultDomain,\n Message,\n Participant,\n MessageAttributes,\n SendMessageResponse,\n} from '@pedi/chika-types';\nimport type { UseChatOptions, UseChatReturn, ChatStatus } from './types';\nimport { ChatDisconnectedError, ChannelClosedError } from './errors';\nimport { createChatSession, type ChatSession, type SessionCallbacks } from './session';\n\nconst DEFAULT_BACKGROUND_GRACE_MS = 2000;\n\n/**\n * React hook for real-time chat over SSE.\n * Manages connection lifecycle, AppState transitions, message deduplication, and reconnection.\n *\n * @template D - Chat domain type for role/message type narrowing. Defaults to DefaultDomain.\n */\nexport function useChat<D extends ChatDomain = DefaultDomain>(\n { config, channelId, profile, onMessage }: UseChatOptions<D>,\n): UseChatReturn<D> {\n const [messages, setMessages] = useState<Message<D>[]>([]);\n const [participants, setParticipants] = useState<Participant<D>[]>([]);\n const [status, setStatus] = useState<ChatStatus>('connecting');\n const [error, setError] = useState<Error | null>(null);\n\n const sessionRef = useRef<ChatSession<D> | null>(null);\n const disposedRef = useRef(false);\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const statusRef = useRef(status);\n statusRef.current = status;\n const appStateRef = useRef<AppStateStatus>(AppState.currentState);\n const backgroundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const profileRef = useRef(profile);\n profileRef.current = profile;\n const configRef = useRef(config);\n configRef.current = config;\n const onMessageRef = useRef(onMessage);\n onMessageRef.current = onMessage;\n const startingRef = useRef(false);\n const pendingOptimisticIds = useRef(new Set<string>());\n\n const backgroundGraceMs =\n config.backgroundGraceMs ?? (Platform.OS === 'android' ? DEFAULT_BACKGROUND_GRACE_MS : 0);\n\n const callbacks: SessionCallbacks<D> = {\n onMessage: (message) => {\n if (disposedRef.current) return;\n setMessages((prev: Message<D>[]) => {\n // Check if this SSE message reconciles a pending optimistic message.\n const optimisticIdx = prev.findIndex(\n (m) =>\n pendingOptimisticIds.current.has(m.id) &&\n m.sender_id === message.sender_id &&\n m.body === message.body &&\n m.type === message.type,\n );\n if (optimisticIdx !== -1) {\n const optimisticId = prev[optimisticIdx]!.id;\n pendingOptimisticIds.current.delete(optimisticId);\n const next = [...prev];\n next[optimisticIdx] = message;\n return next;\n }\n return [...prev, message];\n });\n onMessageRef.current?.(message);\n },\n onStatusChange: (nextStatus) => {\n if (disposedRef.current) return;\n setStatus(nextStatus);\n if (nextStatus === 'connected') setError(null);\n },\n onError: (err) => {\n if (disposedRef.current) return;\n setError(err);\n },\n onResync: () => {\n if (disposedRef.current) return;\n startSession();\n },\n };\n\n async function startSession(): Promise<void> {\n if (startingRef.current) return;\n startingRef.current = true;\n\n const existing = sessionRef.current;\n if (existing) {\n existing.disconnect();\n sessionRef.current = null;\n }\n\n try {\n const session = await createChatSession<D>(configRef.current, channelId, profileRef.current, callbacks);\n\n if (disposedRef.current) {\n session.disconnect();\n return;\n }\n\n sessionRef.current = session;\n setParticipants(session.initialParticipants);\n setMessages(session.initialMessages);\n } catch (err) {\n if (disposedRef.current) return;\n\n if (err instanceof ChannelClosedError) {\n setStatus('closed');\n setError(err);\n return;\n }\n\n setStatus('error');\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n startingRef.current = false;\n }\n }\n\n useEffect(() => {\n disposedRef.current = false;\n startSession();\n\n return () => {\n disposedRef.current = true;\n if (statusRef.current === 'connected' && sessionRef.current) {\n const lastMsg = messagesRef.current[messagesRef.current.length - 1];\n if (lastMsg) {\n sessionRef.current.markAsRead(lastMsg.id).catch(() => {});\n }\n }\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n };\n }, [channelId]);\n\n useEffect(() => {\n function teardownSession(): void {\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n setStatus('disconnected');\n }\n\n const subscription = AppState.addEventListener('change', (nextAppState) => {\n const prev = appStateRef.current;\n appStateRef.current = nextAppState;\n\n if (!sessionRef.current && nextAppState !== 'active') return;\n\n const shouldTeardown =\n nextAppState === 'background' ||\n (Platform.OS === 'ios' && nextAppState === 'inactive');\n\n if (nextAppState === 'active') {\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n return;\n }\n\n if (prev.match(/inactive|background/) && !sessionRef.current) {\n startSession();\n }\n } else if (shouldTeardown) {\n if (backgroundTimerRef.current) return;\n\n if (backgroundGraceMs === 0) {\n teardownSession();\n } else {\n backgroundTimerRef.current = setTimeout(() => {\n backgroundTimerRef.current = null;\n teardownSession();\n }, backgroundGraceMs);\n }\n }\n });\n\n return () => {\n subscription.remove();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [channelId, backgroundGraceMs]);\n\n const sendMessage = useCallback(\n async (type: D['messageType'], body: string, attributes?: MessageAttributes<D>): Promise<SendMessageResponse> => {\n const session = sessionRef.current;\n if (!session) throw new ChatDisconnectedError(statusRef.current);\n\n const optimistic = configRef.current.optimisticSend !== false;\n let optimisticId: string | null = null;\n\n if (optimistic) {\n optimisticId = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n pendingOptimisticIds.current.add(optimisticId);\n const provisionalMsg: Message<D> = {\n id: optimisticId,\n channel_id: channelId,\n sender_id: profileRef.current.id,\n sender_role: profileRef.current.role as D['role'],\n type,\n body,\n attributes: (attributes ?? {}) as MessageAttributes<D>,\n created_at: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, provisionalMsg]);\n }\n\n try {\n const response = await session.sendMessage(type, body, attributes);\n\n if (optimistic && optimisticId) {\n pendingOptimisticIds.current.delete(optimisticId);\n setMessages((prev) => {\n // If SSE already reconciled this message, the optimistic ID is gone.\n const stillPending = prev.some((m) => m.id === optimisticId);\n if (!stillPending) return prev;\n return prev.map((m) =>\n m.id === optimisticId\n ? { ...m, id: response.id, created_at: response.created_at }\n : m,\n );\n });\n }\n\n return response;\n } catch (err) {\n if (optimistic && optimisticId) {\n pendingOptimisticIds.current.delete(optimisticId);\n setMessages((prev) => prev.filter((m) => m.id !== optimisticId));\n }\n throw err;\n }\n },\n [channelId],\n );\n\n const disconnect = useCallback(() => {\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n setStatus('disconnected');\n }, []);\n\n return { messages, participants, status, error, sendMessage, disconnect };\n}\n","import type { ChatStatus } from './types';\n\nexport class ChatDisconnectedError extends Error {\n constructor(public readonly status: ChatStatus) {\n super(`Cannot send message while ${status}`);\n this.name = 'ChatDisconnectedError';\n }\n}\n\nexport class ChannelClosedError extends Error {\n constructor(public readonly channelId: string) {\n super(`Channel ${channelId} is closed`);\n this.name = 'ChannelClosedError';\n }\n}\n","import type { ChatManifest } from '@pedi/chika-types';\n\n/**\n * Creates a single-server manifest. Use this when all channels route to the same server.\n *\n * @example\n * ```ts\n * const config: ChatConfig = { manifest: createManifest('https://chat.example.com') };\n * ```\n */\nexport function createManifest(serverUrl: string): ChatManifest {\n return { buckets: [{ group: 'default', range: [0, 99], server_url: serverUrl }] };\n}\n\nexport function resolveServerUrl(manifest: ChatManifest, channelId: string): string {\n const hash = [...channelId].reduce((sum, c) => sum + c.charCodeAt(0), 0) % 100;\n const bucket = manifest.buckets.find(\n (b) => hash >= b.range[0] && hash <= b.range[1],\n );\n if (!bucket) throw new Error(`No chat bucket for hash ${hash}`);\n return bucket.server_url;\n}\n","import EventSource from 'react-native-sse';\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\n\nexport interface SSEConnectionConfig {\n url: string;\n headers?: Record<string, string>;\n reconnectDelayMs?: number;\n lastEventId?: string;\n customEvents?: string[];\n}\n\nexport interface SSEConnectionCallbacks {\n onOpen?: () => void;\n onEvent: (eventType: string, data: string, lastEventId?: string) => void;\n onError?: (error: Error) => void;\n onClosed?: () => void;\n onReconnecting?: () => void;\n}\n\nexport interface SSEConnection {\n close: () => void;\n}\n\nexport function createSSEConnection(\n config: SSEConnectionConfig,\n callbacks: SSEConnectionCallbacks,\n): SSEConnection {\n const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;\n const customEvents = config.customEvents ?? [];\n\n let currentLastEventId = config.lastEventId;\n let es: EventSource<string> | null = null;\n let disposed = false;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n function cleanup(): void {\n if (es) {\n es.removeAllEventListeners();\n es.close();\n es = null;\n }\n }\n\n function scheduleReconnect(): void {\n if (disposed || reconnectTimer) return;\n callbacks.onReconnecting?.();\n cleanup();\n\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n connect();\n }, reconnectDelay);\n }\n\n function connect(): void {\n if (disposed) return;\n\n es = new EventSource<string>(config.url, {\n headers: {\n ...config.headers,\n ...(currentLastEventId && { 'Last-Event-ID': currentLastEventId }),\n },\n pollingInterval: 0,\n });\n\n es.addEventListener('open', () => {\n if (disposed) return;\n callbacks.onOpen?.();\n });\n\n es.addEventListener('message', (event) => {\n if (disposed || !event.data) return;\n if (event.lastEventId) {\n currentLastEventId = event.lastEventId;\n }\n callbacks.onEvent('message', event.data, event.lastEventId ?? undefined);\n });\n\n for (const eventName of customEvents) {\n es.addEventListener(eventName, (event) => {\n if (disposed) return;\n callbacks.onEvent(eventName, event.data ?? '', undefined);\n });\n }\n\n es.addEventListener('error', (event) => {\n if (disposed) return;\n\n const msg = 'message' in event ? String(event.message) : '';\n\n if (msg.includes('Channel is closed') || msg.includes('410')) {\n callbacks.onClosed?.();\n cleanup();\n disposed = true;\n return;\n }\n\n if (msg) callbacks.onError?.(new Error(msg));\n\n scheduleReconnect();\n });\n\n es.addEventListener('close', () => {\n if (disposed) return;\n scheduleReconnect();\n });\n }\n\n connect();\n\n return {\n close: () => {\n disposed = true;\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n cleanup();\n },\n };\n}\n","import type {\n ChatDomain,\n DefaultDomain,\n Participant,\n Message,\n JoinResponse,\n SendMessageRequest,\n SendMessageResponse,\n MessageAttributes,\n} from '@pedi/chika-types';\nimport type { ChatConfig, ChatStatus } from './types';\nimport { ChatDisconnectedError, ChannelClosedError } from './errors';\nimport { resolveServerUrl } from './resolve-url';\nimport { createSSEConnection, type SSEConnection } from './sse-connection';\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\nconst MAX_SEEN_IDS = 500;\n\nexport interface SessionCallbacks<D extends ChatDomain = DefaultDomain> {\n onMessage: (message: Message<D>) => void;\n onStatusChange: (status: ChatStatus) => void;\n onError: (error: Error) => void;\n onResync: () => void;\n}\n\nexport interface ChatSession<D extends ChatDomain = DefaultDomain> {\n serviceUrl: string;\n channelId: string;\n initialParticipants: Participant<D>[];\n initialMessages: Message<D>[];\n sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;\n markAsRead: (messageId: string) => Promise<void>;\n disconnect: () => void;\n}\n\nexport async function createChatSession<D extends ChatDomain = DefaultDomain>(\n config: ChatConfig,\n channelId: string,\n profile: Participant<D>,\n callbacks: SessionCallbacks<D>,\n): Promise<ChatSession<D>> {\n const serviceUrl = resolveServerUrl(config.manifest, channelId);\n const customHeaders = config.headers ?? {};\n const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;\n\n callbacks.onStatusChange('connecting');\n\n const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(profile),\n });\n\n if (joinRes.status === 410) {\n throw new ChannelClosedError(channelId);\n }\n\n if (!joinRes.ok) {\n throw new Error(`Join failed: ${joinRes.status} ${await joinRes.text()}`);\n }\n\n const { messages, participants, joined_at }: JoinResponse<D> = await joinRes.json();\n\n let lastEventId =\n messages.length > 0 ? messages[messages.length - 1]!.id : undefined;\n\n const joinedAt = joined_at;\n\n const seenMessageIds = new Set<string>(messages.map((m) => m.id));\n\n let sseConn: SSEConnection | null = null;\n let disposed = false;\n\n function trimSeenIds(): void {\n if (seenMessageIds.size <= MAX_SEEN_IDS) return;\n const ids = [...seenMessageIds];\n seenMessageIds.clear();\n for (const id of ids.slice(-MAX_SEEN_IDS)) {\n seenMessageIds.add(id);\n }\n }\n\n function connect(): void {\n if (disposed) return;\n\n const streamUrl = lastEventId\n ? `${serviceUrl}/channels/${channelId}/stream`\n : `${serviceUrl}/channels/${channelId}/stream?since_time=${encodeURIComponent(joinedAt)}`;\n\n sseConn = createSSEConnection(\n {\n url: streamUrl,\n headers: customHeaders,\n reconnectDelayMs: reconnectDelay,\n lastEventId,\n customEvents: ['resync'],\n },\n {\n onOpen: () => {\n if (!disposed) callbacks.onStatusChange('connected');\n },\n onEvent: (eventType, data, eventId) => {\n if (disposed) return;\n\n if (eventType === 'message') {\n let message: Message<D>;\n try {\n message = JSON.parse(data);\n } catch {\n callbacks.onError(new Error('Failed to parse SSE message'));\n return;\n }\n\n if (eventId) {\n lastEventId = eventId;\n }\n\n if (seenMessageIds.has(message.id)) return;\n seenMessageIds.add(message.id);\n trimSeenIds();\n\n callbacks.onMessage(message);\n } else if (eventType === 'resync') {\n sseConn?.close();\n sseConn = null;\n callbacks.onResync();\n }\n },\n onError: (err) => {\n if (!disposed) callbacks.onError(err);\n },\n onClosed: () => {\n callbacks.onStatusChange('closed');\n disposed = true;\n },\n onReconnecting: () => {\n if (!disposed) callbacks.onStatusChange('reconnecting');\n },\n },\n );\n }\n\n connect();\n\n return {\n serviceUrl,\n channelId,\n initialParticipants: participants,\n initialMessages: messages,\n\n sendMessage: async (type, body, attributes) => {\n if (disposed) throw new ChatDisconnectedError('disconnected');\n\n const payload: SendMessageRequest<D> = {\n sender_id: profile.id,\n type,\n body,\n attributes,\n };\n\n const res = await fetch(\n `${serviceUrl}/channels/${channelId}/messages`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(payload),\n },\n );\n\n if (!res.ok) {\n throw new Error(`Send failed: ${res.status} ${await res.text()}`);\n }\n\n const response: SendMessageResponse = await res.json();\n seenMessageIds.add(response.id);\n return response;\n },\n\n markAsRead: async (messageId: string) => {\n const res = await fetch(`${serviceUrl}/channels/${channelId}/read`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify({\n participant_id: profile.id,\n message_id: messageId,\n }),\n });\n if (!res.ok) {\n throw new Error(`markAsRead failed: ${res.status}`);\n }\n },\n\n disconnect: () => {\n disposed = true;\n sseConn?.close();\n sseConn = null;\n callbacks.onStatusChange('disconnected');\n },\n };\n}\n","import { useEffect, useRef, useState, useCallback } from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport type { UnreadCountResponse, SSEUnreadUpdateEvent } from '@pedi/chika-types';\nimport type { ChatConfig } from './types';\nimport { resolveServerUrl } from './resolve-url';\nimport { createSSEConnection, type SSEConnection } from './sse-connection';\n\nconst DEFAULT_BACKGROUND_GRACE_MS = 2000;\nconst UNREAD_CUSTOM_EVENTS = ['unread_snapshot', 'unread_update', 'unread_clear'];\n\nexport interface UseUnreadOptions {\n config: ChatConfig;\n channelId: string;\n participantId: string;\n enabled?: boolean;\n}\n\nexport interface UseUnreadReturn {\n unreadCount: number;\n hasUnread: boolean;\n lastMessageAt: string | null;\n error: Error | null;\n}\n\nexport function useUnread(options: UseUnreadOptions): UseUnreadReturn {\n const { config, channelId, participantId, enabled = true } = options;\n\n const [unreadCount, setUnreadCount] = useState(0);\n const [lastMessageAt, setLastMessageAt] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n const connRef = useRef<SSEConnection | null>(null);\n const configRef = useRef(config);\n configRef.current = config;\n const appStateRef = useRef<AppStateStatus>(AppState.currentState);\n const backgroundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const backgroundGraceMs =\n config.backgroundGraceMs ?? (Platform.OS === 'android' ? DEFAULT_BACKGROUND_GRACE_MS : 0);\n\n const connect = useCallback(() => {\n connRef.current?.close();\n connRef.current = null;\n\n const serviceUrl = resolveServerUrl(configRef.current.manifest, channelId);\n const customHeaders = configRef.current.headers ?? {};\n const url = `${serviceUrl}/channels/${channelId}/unread?participant_id=${encodeURIComponent(participantId)}`;\n\n connRef.current = createSSEConnection(\n {\n url,\n headers: customHeaders,\n reconnectDelayMs: configRef.current.reconnectDelayMs,\n customEvents: UNREAD_CUSTOM_EVENTS,\n },\n {\n onOpen: () => {\n setError(null);\n },\n onEvent: (eventType, data) => {\n try {\n if (eventType === 'unread_snapshot') {\n const snapshot: UnreadCountResponse = JSON.parse(data);\n setUnreadCount(snapshot.unread_count);\n setLastMessageAt(snapshot.last_message_at);\n } else if (eventType === 'unread_update') {\n const update: SSEUnreadUpdateEvent['data'] = JSON.parse(data);\n setUnreadCount((prev) => prev + 1);\n setLastMessageAt(update.created_at);\n } else if (eventType === 'unread_clear') {\n const clear: { channel_id: string; unread_count: number } = JSON.parse(data);\n setUnreadCount(clear.unread_count);\n }\n } catch {\n setError(new Error('Failed to parse unread SSE event'));\n }\n },\n onError: (err) => {\n setError(err);\n },\n onClosed: () => {\n connRef.current = null;\n },\n },\n );\n }, [channelId, participantId]);\n\n const disconnect = useCallback(() => {\n connRef.current?.close();\n connRef.current = null;\n }, []);\n\n useEffect(() => {\n setUnreadCount(0);\n setLastMessageAt(null);\n setError(null);\n\n if (!enabled) {\n disconnect();\n return;\n }\n\n connect();\n\n return () => {\n disconnect();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [channelId, participantId, enabled, connect, disconnect]);\n\n useEffect(() => {\n if (!enabled) return;\n\n const subscription = AppState.addEventListener('change', (nextAppState) => {\n const prev = appStateRef.current;\n appStateRef.current = nextAppState;\n\n if (!connRef.current && nextAppState !== 'active') return;\n\n const shouldTeardown =\n nextAppState === 'background' ||\n (Platform.OS === 'ios' && nextAppState === 'inactive');\n\n if (nextAppState === 'active') {\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n return;\n }\n\n if (prev.match(/inactive|background/) && !connRef.current) {\n connect();\n }\n } else if (shouldTeardown) {\n if (backgroundTimerRef.current) return;\n\n if (backgroundGraceMs === 0) {\n disconnect();\n } else {\n backgroundTimerRef.current = setTimeout(() => {\n backgroundTimerRef.current = null;\n disconnect();\n }, backgroundGraceMs);\n }\n }\n });\n\n return () => {\n subscription.remove();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [enabled, backgroundGraceMs, connect, disconnect]);\n\n return { unreadCount, hasUnread: unreadCount > 0, lastMessageAt, error };\n}\n"],"mappings":"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","names":["EventSource","DEFAULT_RECONNECT_DELAY_MS","import_react","import_react_native","DEFAULT_BACKGROUND_GRACE_MS"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/use-chat.ts","../src/errors.ts","../src/resolve-url.ts","../src/sse-connection.ts","../src/session.ts","../src/use-unread.ts"],"sourcesContent":["export { useChat } from './use-chat';\nexport { useUnread } from './use-unread';\nexport { createChatSession } from './session';\nexport { resolveServerUrl, createManifest } from './resolve-url';\nexport { createSSEConnection } from './sse-connection';\nexport { ChatDisconnectedError, ChannelClosedError } from './errors';\n\nexport type { ChatConfig, ChatStatus, UseChatOptions, UseChatReturn } from './types';\nexport type { ChatSession, SessionCallbacks } from './session';\nexport type { UseUnreadOptions, UseUnreadReturn } from './use-unread';\nexport type { SSEConnection, SSEConnectionConfig, SSEConnectionCallbacks } from './sse-connection';\n\nexport type {\n ChatDomain,\n DefaultDomain,\n Message,\n Participant,\n MessageAttributes,\n SendMessageResponse,\n ChatManifest,\n ChatBucket,\n UnreadCountResponse,\n MarkReadRequest,\n SSEUnreadUpdateEvent,\n SSEUnreadClearEvent,\n SSEUnreadEvent,\n PediChat,\n PediRole,\n PediVehicle,\n PediLocation,\n PediParticipantMeta,\n PediMessageType,\n PediMessageAttributes,\n} from '@pedi/chika-types';\n","import { useEffect, useRef, useState, useCallback } from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport type {\n ChatDomain,\n DefaultDomain,\n Message,\n Participant,\n MessageAttributes,\n SendMessageResponse,\n} from '@pedi/chika-types';\nimport type { UseChatOptions, UseChatReturn, ChatStatus } from './types';\nimport { ChatDisconnectedError, ChannelClosedError } from './errors';\nimport { createChatSession, type ChatSession, type SessionCallbacks } from './session';\n\nconst DEFAULT_BACKGROUND_GRACE_MS = 2000;\n\n/**\n * React hook for real-time chat over SSE.\n * Manages connection lifecycle, AppState transitions, message deduplication, and reconnection.\n *\n * @template D - Chat domain type for role/message type narrowing. Defaults to DefaultDomain.\n */\nexport function useChat<D extends ChatDomain = DefaultDomain>(\n { config, channelId, profile, onMessage }: UseChatOptions<D>,\n): UseChatReturn<D> {\n const [messages, setMessages] = useState<Message<D>[]>([]);\n const [participants, setParticipants] = useState<Participant<D>[]>([]);\n const [status, setStatus] = useState<ChatStatus>('connecting');\n const [error, setError] = useState<Error | null>(null);\n\n const sessionRef = useRef<ChatSession<D> | null>(null);\n const disposedRef = useRef(false);\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const statusRef = useRef(status);\n statusRef.current = status;\n const appStateRef = useRef<AppStateStatus>(AppState.currentState);\n const backgroundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const profileRef = useRef(profile);\n profileRef.current = profile;\n const configRef = useRef(config);\n configRef.current = config;\n const onMessageRef = useRef(onMessage);\n onMessageRef.current = onMessage;\n const startingRef = useRef(false);\n const pendingOptimisticIds = useRef(new Set<string>());\n\n const backgroundGraceMs =\n config.backgroundGraceMs ?? (Platform.OS === 'android' ? DEFAULT_BACKGROUND_GRACE_MS : 0);\n\n const callbacks: SessionCallbacks<D> = {\n onMessage: (message) => {\n if (disposedRef.current) return;\n setMessages((prev: Message<D>[]) => {\n // Fast path: no pending optimistic messages, just append.\n if (pendingOptimisticIds.current.size === 0) {\n return [...prev, message];\n }\n\n // Slow path: check if this SSE message reconciles a pending optimistic message.\n const optimisticIdx = prev.findIndex(\n (m) =>\n pendingOptimisticIds.current.has(m.id) &&\n m.sender_id === message.sender_id &&\n m.body === message.body &&\n m.type === message.type,\n );\n if (optimisticIdx !== -1) {\n const optimisticId = prev[optimisticIdx]!.id;\n pendingOptimisticIds.current.delete(optimisticId);\n const next = [...prev];\n next[optimisticIdx] = message;\n return next;\n }\n return [...prev, message];\n });\n onMessageRef.current?.(message);\n },\n onStatusChange: (nextStatus) => {\n if (disposedRef.current) return;\n setStatus(nextStatus);\n if (nextStatus === 'connected') setError(null);\n },\n onError: (err) => {\n if (disposedRef.current) return;\n setError(err);\n },\n onResync: () => {\n if (disposedRef.current) return;\n startSession();\n },\n };\n\n async function startSession(): Promise<void> {\n if (startingRef.current) return;\n startingRef.current = true;\n\n const existing = sessionRef.current;\n if (existing) {\n existing.disconnect();\n sessionRef.current = null;\n }\n\n try {\n const session = await createChatSession<D>(configRef.current, channelId, profileRef.current, callbacks);\n\n if (disposedRef.current) {\n session.disconnect();\n return;\n }\n\n sessionRef.current = session;\n setParticipants(session.initialParticipants);\n setMessages(session.initialMessages);\n } catch (err) {\n if (disposedRef.current) return;\n\n if (err instanceof ChannelClosedError) {\n setStatus('closed');\n setError(err);\n return;\n }\n\n setStatus('error');\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n startingRef.current = false;\n }\n }\n\n useEffect(() => {\n disposedRef.current = false;\n startSession();\n\n return () => {\n disposedRef.current = true;\n if (statusRef.current === 'connected' && sessionRef.current) {\n const lastMsg = messagesRef.current[messagesRef.current.length - 1];\n if (lastMsg) {\n sessionRef.current.markAsRead(lastMsg.id).catch(() => {});\n }\n }\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n };\n }, [channelId]);\n\n useEffect(() => {\n function teardownSession(): void {\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n setStatus('disconnected');\n }\n\n const subscription = AppState.addEventListener('change', (nextAppState) => {\n const prev = appStateRef.current;\n appStateRef.current = nextAppState;\n\n if (!sessionRef.current && nextAppState !== 'active') return;\n\n const shouldTeardown =\n nextAppState === 'background' ||\n (Platform.OS === 'ios' && nextAppState === 'inactive');\n\n if (nextAppState === 'active') {\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n return;\n }\n\n if (prev.match(/inactive|background/) && !sessionRef.current) {\n startSession();\n }\n } else if (shouldTeardown) {\n if (backgroundTimerRef.current) return;\n\n if (backgroundGraceMs === 0) {\n teardownSession();\n } else {\n backgroundTimerRef.current = setTimeout(() => {\n backgroundTimerRef.current = null;\n teardownSession();\n }, backgroundGraceMs);\n }\n }\n });\n\n return () => {\n subscription.remove();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [channelId, backgroundGraceMs]);\n\n const sendMessage = useCallback(\n async (type: D['messageType'], body: string, attributes?: MessageAttributes<D>): Promise<SendMessageResponse> => {\n const session = sessionRef.current;\n if (!session) throw new ChatDisconnectedError(statusRef.current);\n\n const optimistic = configRef.current.optimisticSend !== false;\n let optimisticId: string | null = null;\n\n if (optimistic) {\n optimisticId = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n pendingOptimisticIds.current.add(optimisticId);\n const provisionalMsg: Message<D> = {\n id: optimisticId,\n channel_id: channelId,\n sender_id: profileRef.current.id,\n sender_role: profileRef.current.role as D['role'],\n type,\n body,\n attributes: (attributes ?? {}) as MessageAttributes<D>,\n created_at: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, provisionalMsg]);\n }\n\n try {\n const response = await session.sendMessage(type, body, attributes);\n\n if (optimistic && optimisticId) {\n pendingOptimisticIds.current.delete(optimisticId);\n setMessages((prev) => {\n // If SSE already reconciled this message, the optimistic ID is gone.\n const stillPending = prev.some((m) => m.id === optimisticId);\n if (!stillPending) return prev;\n return prev.map((m) =>\n m.id === optimisticId\n ? { ...m, id: response.id, created_at: response.created_at }\n : m,\n );\n });\n }\n\n return response;\n } catch (err) {\n if (optimistic && optimisticId) {\n pendingOptimisticIds.current.delete(optimisticId);\n setMessages((prev) => prev.filter((m) => m.id !== optimisticId));\n }\n throw err;\n }\n },\n [channelId],\n );\n\n const disconnect = useCallback(() => {\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n setStatus('disconnected');\n }, []);\n\n return { messages, participants, status, error, sendMessage, disconnect };\n}\n","import type { ChatStatus } from './types';\n\nexport class ChatDisconnectedError extends Error {\n constructor(public readonly status: ChatStatus) {\n super(`Cannot send message while ${status}`);\n this.name = 'ChatDisconnectedError';\n }\n}\n\nexport class ChannelClosedError extends Error {\n constructor(public readonly channelId: string) {\n super(`Channel ${channelId} is closed`);\n this.name = 'ChannelClosedError';\n }\n}\n","import type { ChatManifest } from '@pedi/chika-types';\n\n/**\n * Creates a single-server manifest. Use this when all channels route to the same server.\n *\n * @example\n * ```ts\n * const config: ChatConfig = { manifest: createManifest('https://chat.example.com') };\n * ```\n */\nexport function createManifest(serverUrl: string): ChatManifest {\n return { buckets: [{ group: 'default', range: [0, 99], server_url: serverUrl }] };\n}\n\nexport function resolveServerUrl(manifest: ChatManifest, channelId: string): string {\n const hash = [...channelId].reduce((sum, c) => sum + c.charCodeAt(0), 0) % 100;\n const bucket = manifest.buckets.find(\n (b) => hash >= b.range[0] && hash <= b.range[1],\n );\n if (!bucket) throw new Error(`No chat bucket for hash ${hash}`);\n return bucket.server_url;\n}\n","import EventSource from 'react-native-sse';\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\n\nexport interface SSEConnectionConfig {\n url: string;\n headers?: Record<string, string>;\n reconnectDelayMs?: number;\n lastEventId?: string;\n customEvents?: string[];\n}\n\nexport interface SSEConnectionCallbacks {\n onOpen?: () => void;\n onEvent: (eventType: string, data: string, lastEventId?: string) => void;\n onError?: (error: Error) => void;\n onClosed?: () => void;\n onReconnecting?: () => void;\n}\n\nexport interface SSEConnection {\n close: () => void;\n}\n\nexport function createSSEConnection(\n config: SSEConnectionConfig,\n callbacks: SSEConnectionCallbacks,\n): SSEConnection {\n const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;\n const customEvents = config.customEvents ?? [];\n\n let currentLastEventId = config.lastEventId;\n let es: EventSource<string> | null = null;\n let disposed = false;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n function cleanup(): void {\n if (es) {\n es.removeAllEventListeners();\n es.close();\n es = null;\n }\n }\n\n function scheduleReconnect(): void {\n if (disposed || reconnectTimer) return;\n callbacks.onReconnecting?.();\n cleanup();\n\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n connect();\n }, reconnectDelay);\n }\n\n function connect(): void {\n if (disposed) return;\n\n es = new EventSource<string>(config.url, {\n headers: {\n ...config.headers,\n ...(currentLastEventId && { 'Last-Event-ID': currentLastEventId }),\n },\n pollingInterval: 0,\n });\n\n es.addEventListener('open', () => {\n if (disposed) return;\n callbacks.onOpen?.();\n });\n\n es.addEventListener('message', (event) => {\n if (disposed || !event.data) return;\n if (event.lastEventId) {\n currentLastEventId = event.lastEventId;\n }\n callbacks.onEvent('message', event.data, event.lastEventId ?? undefined);\n });\n\n for (const eventName of customEvents) {\n es.addEventListener(eventName, (event) => {\n if (disposed) return;\n callbacks.onEvent(eventName, event.data ?? '', undefined);\n });\n }\n\n es.addEventListener('error', (event) => {\n if (disposed) return;\n\n const msg = 'message' in event ? String(event.message) : '';\n\n if (msg.includes('Channel is closed') || msg.includes('410')) {\n callbacks.onClosed?.();\n cleanup();\n disposed = true;\n return;\n }\n\n if (msg) callbacks.onError?.(new Error(msg));\n\n scheduleReconnect();\n });\n\n es.addEventListener('close', () => {\n if (disposed) return;\n scheduleReconnect();\n });\n }\n\n connect();\n\n return {\n close: () => {\n disposed = true;\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n cleanup();\n },\n };\n}\n","import type {\n ChatDomain,\n DefaultDomain,\n Participant,\n Message,\n JoinResponse,\n SendMessageRequest,\n SendMessageResponse,\n MessageAttributes,\n} from '@pedi/chika-types';\nimport type { ChatConfig, ChatStatus } from './types';\nimport { ChatDisconnectedError, ChannelClosedError } from './errors';\nimport { resolveServerUrl } from './resolve-url';\nimport { createSSEConnection, type SSEConnection } from './sse-connection';\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\nconst MAX_SEEN_IDS = 500;\n\nexport interface SessionCallbacks<D extends ChatDomain = DefaultDomain> {\n onMessage: (message: Message<D>) => void;\n onStatusChange: (status: ChatStatus) => void;\n onError: (error: Error) => void;\n onResync: () => void;\n}\n\nexport interface ChatSession<D extends ChatDomain = DefaultDomain> {\n serviceUrl: string;\n channelId: string;\n initialParticipants: Participant<D>[];\n initialMessages: Message<D>[];\n sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;\n markAsRead: (messageId: string) => Promise<void>;\n disconnect: () => void;\n}\n\nexport async function createChatSession<D extends ChatDomain = DefaultDomain>(\n config: ChatConfig,\n channelId: string,\n profile: Participant<D>,\n callbacks: SessionCallbacks<D>,\n): Promise<ChatSession<D>> {\n const serviceUrl = resolveServerUrl(config.manifest, channelId);\n const customHeaders = config.headers ?? {};\n const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;\n\n callbacks.onStatusChange('connecting');\n\n const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(profile),\n });\n\n if (joinRes.status === 410) {\n throw new ChannelClosedError(channelId);\n }\n\n if (!joinRes.ok) {\n throw new Error(`Join failed: ${joinRes.status} ${await joinRes.text()}`);\n }\n\n const { messages, participants, joined_at }: JoinResponse<D> = await joinRes.json();\n\n let lastEventId =\n messages.length > 0 ? messages[messages.length - 1]!.id : undefined;\n\n const joinedAt = joined_at;\n\n const seenMessageIds = new Set<string>(messages.map((m) => m.id));\n\n let sseConn: SSEConnection | null = null;\n let disposed = false;\n\n const TRIM_THRESHOLD = MAX_SEEN_IDS * 1.5;\n\n function trimSeenIds(): void {\n if (seenMessageIds.size <= TRIM_THRESHOLD) return;\n const ids = [...seenMessageIds];\n seenMessageIds.clear();\n for (const id of ids.slice(-MAX_SEEN_IDS)) {\n seenMessageIds.add(id);\n }\n }\n\n function connect(): void {\n if (disposed) return;\n\n const streamUrl = lastEventId\n ? `${serviceUrl}/channels/${channelId}/stream`\n : `${serviceUrl}/channels/${channelId}/stream?since_time=${encodeURIComponent(joinedAt)}`;\n\n sseConn = createSSEConnection(\n {\n url: streamUrl,\n headers: customHeaders,\n reconnectDelayMs: reconnectDelay,\n lastEventId,\n customEvents: ['resync'],\n },\n {\n onOpen: () => {\n if (!disposed) callbacks.onStatusChange('connected');\n },\n onEvent: (eventType, data, eventId) => {\n if (disposed) return;\n\n if (eventType === 'message') {\n let message: Message<D>;\n try {\n message = JSON.parse(data);\n } catch {\n callbacks.onError(new Error('Failed to parse SSE message'));\n return;\n }\n\n if (eventId) {\n lastEventId = eventId;\n }\n\n if (seenMessageIds.has(message.id)) return;\n seenMessageIds.add(message.id);\n trimSeenIds();\n\n callbacks.onMessage(message);\n } else if (eventType === 'resync') {\n sseConn?.close();\n sseConn = null;\n callbacks.onResync();\n }\n },\n onError: (err) => {\n if (!disposed) callbacks.onError(err);\n },\n onClosed: () => {\n callbacks.onStatusChange('closed');\n disposed = true;\n },\n onReconnecting: () => {\n if (!disposed) callbacks.onStatusChange('reconnecting');\n },\n },\n );\n }\n\n connect();\n\n return {\n serviceUrl,\n channelId,\n initialParticipants: participants,\n initialMessages: messages,\n\n sendMessage: async (type, body, attributes) => {\n if (disposed) throw new ChatDisconnectedError('disconnected');\n\n const payload: SendMessageRequest<D> = {\n sender_id: profile.id,\n type,\n body,\n attributes,\n };\n\n const res = await fetch(\n `${serviceUrl}/channels/${channelId}/messages`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(payload),\n },\n );\n\n if (!res.ok) {\n throw new Error(`Send failed: ${res.status} ${await res.text()}`);\n }\n\n const response: SendMessageResponse = await res.json();\n seenMessageIds.add(response.id);\n return response;\n },\n\n markAsRead: async (messageId: string) => {\n const res = await fetch(`${serviceUrl}/channels/${channelId}/read`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify({\n participant_id: profile.id,\n message_id: messageId,\n }),\n });\n if (!res.ok) {\n throw new Error(`markAsRead failed: ${res.status}`);\n }\n },\n\n disconnect: () => {\n disposed = true;\n sseConn?.close();\n sseConn = null;\n callbacks.onStatusChange('disconnected');\n },\n };\n}\n","import { useEffect, useRef, useState, useCallback } from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport type { UnreadCountResponse, SSEUnreadUpdateEvent } from '@pedi/chika-types';\nimport type { ChatConfig } from './types';\nimport { resolveServerUrl } from './resolve-url';\nimport { createSSEConnection, type SSEConnection } from './sse-connection';\n\nconst DEFAULT_BACKGROUND_GRACE_MS = 2000;\nconst UNREAD_CUSTOM_EVENTS = ['unread_snapshot', 'unread_update', 'unread_clear'];\n\nexport interface UseUnreadOptions {\n config: ChatConfig;\n channelId: string;\n participantId: string;\n enabled?: boolean;\n}\n\nexport interface UseUnreadReturn {\n unreadCount: number;\n hasUnread: boolean;\n lastMessageAt: string | null;\n error: Error | null;\n}\n\nexport function useUnread(options: UseUnreadOptions): UseUnreadReturn {\n const { config, channelId, participantId, enabled = true } = options;\n\n const [unreadCount, setUnreadCount] = useState(0);\n const [lastMessageAt, setLastMessageAt] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n const connRef = useRef<SSEConnection | null>(null);\n const configRef = useRef(config);\n configRef.current = config;\n const appStateRef = useRef<AppStateStatus>(AppState.currentState);\n const backgroundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const backgroundGraceMs =\n config.backgroundGraceMs ?? (Platform.OS === 'android' ? DEFAULT_BACKGROUND_GRACE_MS : 0);\n\n const connect = useCallback(() => {\n connRef.current?.close();\n connRef.current = null;\n\n const serviceUrl = resolveServerUrl(configRef.current.manifest, channelId);\n const customHeaders = configRef.current.headers ?? {};\n const url = `${serviceUrl}/channels/${channelId}/unread?participant_id=${encodeURIComponent(participantId)}`;\n\n connRef.current = createSSEConnection(\n {\n url,\n headers: customHeaders,\n reconnectDelayMs: configRef.current.reconnectDelayMs,\n customEvents: UNREAD_CUSTOM_EVENTS,\n },\n {\n onOpen: () => {\n setError(null);\n },\n onEvent: (eventType, data) => {\n try {\n if (eventType === 'unread_snapshot') {\n const snapshot: UnreadCountResponse = JSON.parse(data);\n setUnreadCount(snapshot.unread_count);\n setLastMessageAt(snapshot.last_message_at);\n } else if (eventType === 'unread_update') {\n const update: SSEUnreadUpdateEvent['data'] = JSON.parse(data);\n setUnreadCount((prev) => prev + 1);\n setLastMessageAt(update.created_at);\n } else if (eventType === 'unread_clear') {\n const clear: { channel_id: string; unread_count: number } = JSON.parse(data);\n setUnreadCount(clear.unread_count);\n }\n } catch {\n setError(new Error('Failed to parse unread SSE event'));\n }\n },\n onError: (err) => {\n setError(err);\n },\n onClosed: () => {\n connRef.current = null;\n },\n },\n );\n }, [channelId, participantId]);\n\n const disconnect = useCallback(() => {\n connRef.current?.close();\n connRef.current = null;\n }, []);\n\n useEffect(() => {\n setUnreadCount(0);\n setLastMessageAt(null);\n setError(null);\n\n if (!enabled) {\n disconnect();\n return;\n }\n\n connect();\n\n return () => {\n disconnect();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [channelId, participantId, enabled, connect, disconnect]);\n\n useEffect(() => {\n if (!enabled) return;\n\n const subscription = AppState.addEventListener('change', (nextAppState) => {\n const prev = appStateRef.current;\n appStateRef.current = nextAppState;\n\n if (!connRef.current && nextAppState !== 'active') return;\n\n const shouldTeardown =\n nextAppState === 'background' ||\n (Platform.OS === 'ios' && nextAppState === 'inactive');\n\n if (nextAppState === 'active') {\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n return;\n }\n\n if (prev.match(/inactive|background/) && !connRef.current) {\n connect();\n }\n } else if (shouldTeardown) {\n if (backgroundTimerRef.current) return;\n\n if (backgroundGraceMs === 0) {\n disconnect();\n } else {\n backgroundTimerRef.current = setTimeout(() => {\n backgroundTimerRef.current = null;\n disconnect();\n }, backgroundGraceMs);\n }\n }\n });\n\n return () => {\n subscription.remove();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [enabled, backgroundGraceMs, connect, disconnect]);\n\n return { unreadCount, hasUnread: unreadCount > 0, lastMessageAt, error };\n}\n"],"mappings":"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","names":["EventSource","DEFAULT_RECONNECT_DELAY_MS","import_react","import_react_native","DEFAULT_BACKGROUND_GRACE_MS"]}
|
package/dist/index.mjs
CHANGED
|
@@ -138,8 +138,9 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
138
138
|
const seenMessageIds = new Set(messages.map((m) => m.id));
|
|
139
139
|
let sseConn = null;
|
|
140
140
|
let disposed = false;
|
|
141
|
+
const TRIM_THRESHOLD = MAX_SEEN_IDS * 1.5;
|
|
141
142
|
function trimSeenIds() {
|
|
142
|
-
if (seenMessageIds.size <=
|
|
143
|
+
if (seenMessageIds.size <= TRIM_THRESHOLD) return;
|
|
143
144
|
const ids = [...seenMessageIds];
|
|
144
145
|
seenMessageIds.clear();
|
|
145
146
|
for (const id of ids.slice(-MAX_SEEN_IDS)) {
|
|
@@ -276,6 +277,9 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
276
277
|
onMessage: (message) => {
|
|
277
278
|
if (disposedRef.current) return;
|
|
278
279
|
setMessages((prev) => {
|
|
280
|
+
if (pendingOptimisticIds.current.size === 0) {
|
|
281
|
+
return [...prev, message];
|
|
282
|
+
}
|
|
279
283
|
const optimisticIdx = prev.findIndex(
|
|
280
284
|
(m) => pendingOptimisticIds.current.has(m.id) && m.sender_id === message.sender_id && m.body === message.body && m.type === message.type
|
|
281
285
|
);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/use-chat.ts","../src/errors.ts","../src/resolve-url.ts","../src/sse-connection.ts","../src/session.ts","../src/use-unread.ts"],"sourcesContent":["import { useEffect, useRef, useState, useCallback } from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport type {\n ChatDomain,\n DefaultDomain,\n Message,\n Participant,\n MessageAttributes,\n SendMessageResponse,\n} from '@pedi/chika-types';\nimport type { UseChatOptions, UseChatReturn, ChatStatus } from './types';\nimport { ChatDisconnectedError, ChannelClosedError } from './errors';\nimport { createChatSession, type ChatSession, type SessionCallbacks } from './session';\n\nconst DEFAULT_BACKGROUND_GRACE_MS = 2000;\n\n/**\n * React hook for real-time chat over SSE.\n * Manages connection lifecycle, AppState transitions, message deduplication, and reconnection.\n *\n * @template D - Chat domain type for role/message type narrowing. Defaults to DefaultDomain.\n */\nexport function useChat<D extends ChatDomain = DefaultDomain>(\n { config, channelId, profile, onMessage }: UseChatOptions<D>,\n): UseChatReturn<D> {\n const [messages, setMessages] = useState<Message<D>[]>([]);\n const [participants, setParticipants] = useState<Participant<D>[]>([]);\n const [status, setStatus] = useState<ChatStatus>('connecting');\n const [error, setError] = useState<Error | null>(null);\n\n const sessionRef = useRef<ChatSession<D> | null>(null);\n const disposedRef = useRef(false);\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const statusRef = useRef(status);\n statusRef.current = status;\n const appStateRef = useRef<AppStateStatus>(AppState.currentState);\n const backgroundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const profileRef = useRef(profile);\n profileRef.current = profile;\n const configRef = useRef(config);\n configRef.current = config;\n const onMessageRef = useRef(onMessage);\n onMessageRef.current = onMessage;\n const startingRef = useRef(false);\n const pendingOptimisticIds = useRef(new Set<string>());\n\n const backgroundGraceMs =\n config.backgroundGraceMs ?? (Platform.OS === 'android' ? DEFAULT_BACKGROUND_GRACE_MS : 0);\n\n const callbacks: SessionCallbacks<D> = {\n onMessage: (message) => {\n if (disposedRef.current) return;\n setMessages((prev: Message<D>[]) => {\n // Check if this SSE message reconciles a pending optimistic message.\n const optimisticIdx = prev.findIndex(\n (m) =>\n pendingOptimisticIds.current.has(m.id) &&\n m.sender_id === message.sender_id &&\n m.body === message.body &&\n m.type === message.type,\n );\n if (optimisticIdx !== -1) {\n const optimisticId = prev[optimisticIdx]!.id;\n pendingOptimisticIds.current.delete(optimisticId);\n const next = [...prev];\n next[optimisticIdx] = message;\n return next;\n }\n return [...prev, message];\n });\n onMessageRef.current?.(message);\n },\n onStatusChange: (nextStatus) => {\n if (disposedRef.current) return;\n setStatus(nextStatus);\n if (nextStatus === 'connected') setError(null);\n },\n onError: (err) => {\n if (disposedRef.current) return;\n setError(err);\n },\n onResync: () => {\n if (disposedRef.current) return;\n startSession();\n },\n };\n\n async function startSession(): Promise<void> {\n if (startingRef.current) return;\n startingRef.current = true;\n\n const existing = sessionRef.current;\n if (existing) {\n existing.disconnect();\n sessionRef.current = null;\n }\n\n try {\n const session = await createChatSession<D>(configRef.current, channelId, profileRef.current, callbacks);\n\n if (disposedRef.current) {\n session.disconnect();\n return;\n }\n\n sessionRef.current = session;\n setParticipants(session.initialParticipants);\n setMessages(session.initialMessages);\n } catch (err) {\n if (disposedRef.current) return;\n\n if (err instanceof ChannelClosedError) {\n setStatus('closed');\n setError(err);\n return;\n }\n\n setStatus('error');\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n startingRef.current = false;\n }\n }\n\n useEffect(() => {\n disposedRef.current = false;\n startSession();\n\n return () => {\n disposedRef.current = true;\n if (statusRef.current === 'connected' && sessionRef.current) {\n const lastMsg = messagesRef.current[messagesRef.current.length - 1];\n if (lastMsg) {\n sessionRef.current.markAsRead(lastMsg.id).catch(() => {});\n }\n }\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n };\n }, [channelId]);\n\n useEffect(() => {\n function teardownSession(): void {\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n setStatus('disconnected');\n }\n\n const subscription = AppState.addEventListener('change', (nextAppState) => {\n const prev = appStateRef.current;\n appStateRef.current = nextAppState;\n\n if (!sessionRef.current && nextAppState !== 'active') return;\n\n const shouldTeardown =\n nextAppState === 'background' ||\n (Platform.OS === 'ios' && nextAppState === 'inactive');\n\n if (nextAppState === 'active') {\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n return;\n }\n\n if (prev.match(/inactive|background/) && !sessionRef.current) {\n startSession();\n }\n } else if (shouldTeardown) {\n if (backgroundTimerRef.current) return;\n\n if (backgroundGraceMs === 0) {\n teardownSession();\n } else {\n backgroundTimerRef.current = setTimeout(() => {\n backgroundTimerRef.current = null;\n teardownSession();\n }, backgroundGraceMs);\n }\n }\n });\n\n return () => {\n subscription.remove();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [channelId, backgroundGraceMs]);\n\n const sendMessage = useCallback(\n async (type: D['messageType'], body: string, attributes?: MessageAttributes<D>): Promise<SendMessageResponse> => {\n const session = sessionRef.current;\n if (!session) throw new ChatDisconnectedError(statusRef.current);\n\n const optimistic = configRef.current.optimisticSend !== false;\n let optimisticId: string | null = null;\n\n if (optimistic) {\n optimisticId = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n pendingOptimisticIds.current.add(optimisticId);\n const provisionalMsg: Message<D> = {\n id: optimisticId,\n channel_id: channelId,\n sender_id: profileRef.current.id,\n sender_role: profileRef.current.role as D['role'],\n type,\n body,\n attributes: (attributes ?? {}) as MessageAttributes<D>,\n created_at: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, provisionalMsg]);\n }\n\n try {\n const response = await session.sendMessage(type, body, attributes);\n\n if (optimistic && optimisticId) {\n pendingOptimisticIds.current.delete(optimisticId);\n setMessages((prev) => {\n // If SSE already reconciled this message, the optimistic ID is gone.\n const stillPending = prev.some((m) => m.id === optimisticId);\n if (!stillPending) return prev;\n return prev.map((m) =>\n m.id === optimisticId\n ? { ...m, id: response.id, created_at: response.created_at }\n : m,\n );\n });\n }\n\n return response;\n } catch (err) {\n if (optimistic && optimisticId) {\n pendingOptimisticIds.current.delete(optimisticId);\n setMessages((prev) => prev.filter((m) => m.id !== optimisticId));\n }\n throw err;\n }\n },\n [channelId],\n );\n\n const disconnect = useCallback(() => {\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n setStatus('disconnected');\n }, []);\n\n return { messages, participants, status, error, sendMessage, disconnect };\n}\n","import type { ChatStatus } from './types';\n\nexport class ChatDisconnectedError extends Error {\n constructor(public readonly status: ChatStatus) {\n super(`Cannot send message while ${status}`);\n this.name = 'ChatDisconnectedError';\n }\n}\n\nexport class ChannelClosedError extends Error {\n constructor(public readonly channelId: string) {\n super(`Channel ${channelId} is closed`);\n this.name = 'ChannelClosedError';\n }\n}\n","import type { ChatManifest } from '@pedi/chika-types';\n\n/**\n * Creates a single-server manifest. Use this when all channels route to the same server.\n *\n * @example\n * ```ts\n * const config: ChatConfig = { manifest: createManifest('https://chat.example.com') };\n * ```\n */\nexport function createManifest(serverUrl: string): ChatManifest {\n return { buckets: [{ group: 'default', range: [0, 99], server_url: serverUrl }] };\n}\n\nexport function resolveServerUrl(manifest: ChatManifest, channelId: string): string {\n const hash = [...channelId].reduce((sum, c) => sum + c.charCodeAt(0), 0) % 100;\n const bucket = manifest.buckets.find(\n (b) => hash >= b.range[0] && hash <= b.range[1],\n );\n if (!bucket) throw new Error(`No chat bucket for hash ${hash}`);\n return bucket.server_url;\n}\n","import EventSource from 'react-native-sse';\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\n\nexport interface SSEConnectionConfig {\n url: string;\n headers?: Record<string, string>;\n reconnectDelayMs?: number;\n lastEventId?: string;\n customEvents?: string[];\n}\n\nexport interface SSEConnectionCallbacks {\n onOpen?: () => void;\n onEvent: (eventType: string, data: string, lastEventId?: string) => void;\n onError?: (error: Error) => void;\n onClosed?: () => void;\n onReconnecting?: () => void;\n}\n\nexport interface SSEConnection {\n close: () => void;\n}\n\nexport function createSSEConnection(\n config: SSEConnectionConfig,\n callbacks: SSEConnectionCallbacks,\n): SSEConnection {\n const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;\n const customEvents = config.customEvents ?? [];\n\n let currentLastEventId = config.lastEventId;\n let es: EventSource<string> | null = null;\n let disposed = false;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n function cleanup(): void {\n if (es) {\n es.removeAllEventListeners();\n es.close();\n es = null;\n }\n }\n\n function scheduleReconnect(): void {\n if (disposed || reconnectTimer) return;\n callbacks.onReconnecting?.();\n cleanup();\n\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n connect();\n }, reconnectDelay);\n }\n\n function connect(): void {\n if (disposed) return;\n\n es = new EventSource<string>(config.url, {\n headers: {\n ...config.headers,\n ...(currentLastEventId && { 'Last-Event-ID': currentLastEventId }),\n },\n pollingInterval: 0,\n });\n\n es.addEventListener('open', () => {\n if (disposed) return;\n callbacks.onOpen?.();\n });\n\n es.addEventListener('message', (event) => {\n if (disposed || !event.data) return;\n if (event.lastEventId) {\n currentLastEventId = event.lastEventId;\n }\n callbacks.onEvent('message', event.data, event.lastEventId ?? undefined);\n });\n\n for (const eventName of customEvents) {\n es.addEventListener(eventName, (event) => {\n if (disposed) return;\n callbacks.onEvent(eventName, event.data ?? '', undefined);\n });\n }\n\n es.addEventListener('error', (event) => {\n if (disposed) return;\n\n const msg = 'message' in event ? String(event.message) : '';\n\n if (msg.includes('Channel is closed') || msg.includes('410')) {\n callbacks.onClosed?.();\n cleanup();\n disposed = true;\n return;\n }\n\n if (msg) callbacks.onError?.(new Error(msg));\n\n scheduleReconnect();\n });\n\n es.addEventListener('close', () => {\n if (disposed) return;\n scheduleReconnect();\n });\n }\n\n connect();\n\n return {\n close: () => {\n disposed = true;\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n cleanup();\n },\n };\n}\n","import type {\n ChatDomain,\n DefaultDomain,\n Participant,\n Message,\n JoinResponse,\n SendMessageRequest,\n SendMessageResponse,\n MessageAttributes,\n} from '@pedi/chika-types';\nimport type { ChatConfig, ChatStatus } from './types';\nimport { ChatDisconnectedError, ChannelClosedError } from './errors';\nimport { resolveServerUrl } from './resolve-url';\nimport { createSSEConnection, type SSEConnection } from './sse-connection';\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\nconst MAX_SEEN_IDS = 500;\n\nexport interface SessionCallbacks<D extends ChatDomain = DefaultDomain> {\n onMessage: (message: Message<D>) => void;\n onStatusChange: (status: ChatStatus) => void;\n onError: (error: Error) => void;\n onResync: () => void;\n}\n\nexport interface ChatSession<D extends ChatDomain = DefaultDomain> {\n serviceUrl: string;\n channelId: string;\n initialParticipants: Participant<D>[];\n initialMessages: Message<D>[];\n sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;\n markAsRead: (messageId: string) => Promise<void>;\n disconnect: () => void;\n}\n\nexport async function createChatSession<D extends ChatDomain = DefaultDomain>(\n config: ChatConfig,\n channelId: string,\n profile: Participant<D>,\n callbacks: SessionCallbacks<D>,\n): Promise<ChatSession<D>> {\n const serviceUrl = resolveServerUrl(config.manifest, channelId);\n const customHeaders = config.headers ?? {};\n const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;\n\n callbacks.onStatusChange('connecting');\n\n const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(profile),\n });\n\n if (joinRes.status === 410) {\n throw new ChannelClosedError(channelId);\n }\n\n if (!joinRes.ok) {\n throw new Error(`Join failed: ${joinRes.status} ${await joinRes.text()}`);\n }\n\n const { messages, participants, joined_at }: JoinResponse<D> = await joinRes.json();\n\n let lastEventId =\n messages.length > 0 ? messages[messages.length - 1]!.id : undefined;\n\n const joinedAt = joined_at;\n\n const seenMessageIds = new Set<string>(messages.map((m) => m.id));\n\n let sseConn: SSEConnection | null = null;\n let disposed = false;\n\n function trimSeenIds(): void {\n if (seenMessageIds.size <= MAX_SEEN_IDS) return;\n const ids = [...seenMessageIds];\n seenMessageIds.clear();\n for (const id of ids.slice(-MAX_SEEN_IDS)) {\n seenMessageIds.add(id);\n }\n }\n\n function connect(): void {\n if (disposed) return;\n\n const streamUrl = lastEventId\n ? `${serviceUrl}/channels/${channelId}/stream`\n : `${serviceUrl}/channels/${channelId}/stream?since_time=${encodeURIComponent(joinedAt)}`;\n\n sseConn = createSSEConnection(\n {\n url: streamUrl,\n headers: customHeaders,\n reconnectDelayMs: reconnectDelay,\n lastEventId,\n customEvents: ['resync'],\n },\n {\n onOpen: () => {\n if (!disposed) callbacks.onStatusChange('connected');\n },\n onEvent: (eventType, data, eventId) => {\n if (disposed) return;\n\n if (eventType === 'message') {\n let message: Message<D>;\n try {\n message = JSON.parse(data);\n } catch {\n callbacks.onError(new Error('Failed to parse SSE message'));\n return;\n }\n\n if (eventId) {\n lastEventId = eventId;\n }\n\n if (seenMessageIds.has(message.id)) return;\n seenMessageIds.add(message.id);\n trimSeenIds();\n\n callbacks.onMessage(message);\n } else if (eventType === 'resync') {\n sseConn?.close();\n sseConn = null;\n callbacks.onResync();\n }\n },\n onError: (err) => {\n if (!disposed) callbacks.onError(err);\n },\n onClosed: () => {\n callbacks.onStatusChange('closed');\n disposed = true;\n },\n onReconnecting: () => {\n if (!disposed) callbacks.onStatusChange('reconnecting');\n },\n },\n );\n }\n\n connect();\n\n return {\n serviceUrl,\n channelId,\n initialParticipants: participants,\n initialMessages: messages,\n\n sendMessage: async (type, body, attributes) => {\n if (disposed) throw new ChatDisconnectedError('disconnected');\n\n const payload: SendMessageRequest<D> = {\n sender_id: profile.id,\n type,\n body,\n attributes,\n };\n\n const res = await fetch(\n `${serviceUrl}/channels/${channelId}/messages`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(payload),\n },\n );\n\n if (!res.ok) {\n throw new Error(`Send failed: ${res.status} ${await res.text()}`);\n }\n\n const response: SendMessageResponse = await res.json();\n seenMessageIds.add(response.id);\n return response;\n },\n\n markAsRead: async (messageId: string) => {\n const res = await fetch(`${serviceUrl}/channels/${channelId}/read`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify({\n participant_id: profile.id,\n message_id: messageId,\n }),\n });\n if (!res.ok) {\n throw new Error(`markAsRead failed: ${res.status}`);\n }\n },\n\n disconnect: () => {\n disposed = true;\n sseConn?.close();\n sseConn = null;\n callbacks.onStatusChange('disconnected');\n },\n };\n}\n","import { useEffect, useRef, useState, useCallback } from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport type { UnreadCountResponse, SSEUnreadUpdateEvent } from '@pedi/chika-types';\nimport type { ChatConfig } from './types';\nimport { resolveServerUrl } from './resolve-url';\nimport { createSSEConnection, type SSEConnection } from './sse-connection';\n\nconst DEFAULT_BACKGROUND_GRACE_MS = 2000;\nconst UNREAD_CUSTOM_EVENTS = ['unread_snapshot', 'unread_update', 'unread_clear'];\n\nexport interface UseUnreadOptions {\n config: ChatConfig;\n channelId: string;\n participantId: string;\n enabled?: boolean;\n}\n\nexport interface UseUnreadReturn {\n unreadCount: number;\n hasUnread: boolean;\n lastMessageAt: string | null;\n error: Error | null;\n}\n\nexport function useUnread(options: UseUnreadOptions): UseUnreadReturn {\n const { config, channelId, participantId, enabled = true } = options;\n\n const [unreadCount, setUnreadCount] = useState(0);\n const [lastMessageAt, setLastMessageAt] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n const connRef = useRef<SSEConnection | null>(null);\n const configRef = useRef(config);\n configRef.current = config;\n const appStateRef = useRef<AppStateStatus>(AppState.currentState);\n const backgroundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const backgroundGraceMs =\n config.backgroundGraceMs ?? (Platform.OS === 'android' ? DEFAULT_BACKGROUND_GRACE_MS : 0);\n\n const connect = useCallback(() => {\n connRef.current?.close();\n connRef.current = null;\n\n const serviceUrl = resolveServerUrl(configRef.current.manifest, channelId);\n const customHeaders = configRef.current.headers ?? {};\n const url = `${serviceUrl}/channels/${channelId}/unread?participant_id=${encodeURIComponent(participantId)}`;\n\n connRef.current = createSSEConnection(\n {\n url,\n headers: customHeaders,\n reconnectDelayMs: configRef.current.reconnectDelayMs,\n customEvents: UNREAD_CUSTOM_EVENTS,\n },\n {\n onOpen: () => {\n setError(null);\n },\n onEvent: (eventType, data) => {\n try {\n if (eventType === 'unread_snapshot') {\n const snapshot: UnreadCountResponse = JSON.parse(data);\n setUnreadCount(snapshot.unread_count);\n setLastMessageAt(snapshot.last_message_at);\n } else if (eventType === 'unread_update') {\n const update: SSEUnreadUpdateEvent['data'] = JSON.parse(data);\n setUnreadCount((prev) => prev + 1);\n setLastMessageAt(update.created_at);\n } else if (eventType === 'unread_clear') {\n const clear: { channel_id: string; unread_count: number } = JSON.parse(data);\n setUnreadCount(clear.unread_count);\n }\n } catch {\n setError(new Error('Failed to parse unread SSE event'));\n }\n },\n onError: (err) => {\n setError(err);\n },\n onClosed: () => {\n connRef.current = null;\n },\n },\n );\n }, [channelId, participantId]);\n\n const disconnect = useCallback(() => {\n connRef.current?.close();\n connRef.current = null;\n }, []);\n\n useEffect(() => {\n setUnreadCount(0);\n setLastMessageAt(null);\n setError(null);\n\n if (!enabled) {\n disconnect();\n return;\n }\n\n connect();\n\n return () => {\n disconnect();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [channelId, participantId, enabled, connect, disconnect]);\n\n useEffect(() => {\n if (!enabled) return;\n\n const subscription = AppState.addEventListener('change', (nextAppState) => {\n const prev = appStateRef.current;\n appStateRef.current = nextAppState;\n\n if (!connRef.current && nextAppState !== 'active') return;\n\n const shouldTeardown =\n nextAppState === 'background' ||\n (Platform.OS === 'ios' && nextAppState === 'inactive');\n\n if (nextAppState === 'active') {\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n return;\n }\n\n if (prev.match(/inactive|background/) && !connRef.current) {\n connect();\n }\n } else if (shouldTeardown) {\n if (backgroundTimerRef.current) return;\n\n if (backgroundGraceMs === 0) {\n disconnect();\n } else {\n backgroundTimerRef.current = setTimeout(() => {\n backgroundTimerRef.current = null;\n disconnect();\n }, backgroundGraceMs);\n }\n }\n });\n\n return () => {\n subscription.remove();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [enabled, backgroundGraceMs, connect, disconnect]);\n\n return { unreadCount, hasUnread: unreadCount > 0, lastMessageAt, error };\n}\n"],"mappings":";AAAA,SAAS,WAAW,QAAQ,UAAU,mBAAmB;AACzD,SAAS,UAAU,gBAAqC;;;ACCjD,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC/C,YAA4B,QAAoB;AAC9C,UAAM,6BAA6B,MAAM,EAAE;AADjB;AAE1B,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,qBAAN,cAAiC,MAAM;AAAA,EAC5C,YAA4B,WAAmB;AAC7C,UAAM,WAAW,SAAS,YAAY;AADZ;AAE1B,SAAK,OAAO;AAAA,EACd;AACF;;;ACJO,SAAS,eAAe,WAAiC;AAC9D,SAAO,EAAE,SAAS,CAAC,EAAE,OAAO,WAAW,OAAO,CAAC,GAAG,EAAE,GAAG,YAAY,UAAU,CAAC,EAAE;AAClF;AAEO,SAAS,iBAAiB,UAAwB,WAA2B;AAClF,QAAM,OAAO,CAAC,GAAG,SAAS,EAAE,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,IAAI;AAC3E,QAAM,SAAS,SAAS,QAAQ;AAAA,IAC9B,CAAC,MAAM,QAAQ,EAAE,MAAM,CAAC,KAAK,QAAQ,EAAE,MAAM,CAAC;AAAA,EAChD;AACA,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,2BAA2B,IAAI,EAAE;AAC9D,SAAO,OAAO;AAChB;;;ACrBA,OAAO,iBAAiB;AAExB,IAAM,6BAA6B;AAsB5B,SAAS,oBACd,QACA,WACe;AACf,QAAM,iBAAiB,OAAO,oBAAoB;AAClD,QAAM,eAAe,OAAO,gBAAgB,CAAC;AAE7C,MAAI,qBAAqB,OAAO;AAChC,MAAI,KAAiC;AACrC,MAAI,WAAW;AACf,MAAI,iBAAuD;AAE3D,WAAS,UAAgB;AACvB,QAAI,IAAI;AACN,SAAG,wBAAwB;AAC3B,SAAG,MAAM;AACT,WAAK;AAAA,IACP;AAAA,EACF;AAEA,WAAS,oBAA0B;AACjC,QAAI,YAAY,eAAgB;AAChC,cAAU,iBAAiB;AAC3B,YAAQ;AAER,qBAAiB,WAAW,MAAM;AAChC,uBAAiB;AACjB,cAAQ;AAAA,IACV,GAAG,cAAc;AAAA,EACnB;AAEA,WAAS,UAAgB;AACvB,QAAI,SAAU;AAEd,SAAK,IAAI,YAAoB,OAAO,KAAK;AAAA,MACvC,SAAS;AAAA,QACP,GAAG,OAAO;AAAA,QACV,GAAI,sBAAsB,EAAE,iBAAiB,mBAAmB;AAAA,MAClE;AAAA,MACA,iBAAiB;AAAA,IACnB,CAAC;AAED,OAAG,iBAAiB,QAAQ,MAAM;AAChC,UAAI,SAAU;AACd,gBAAU,SAAS;AAAA,IACrB,CAAC;AAED,OAAG,iBAAiB,WAAW,CAAC,UAAU;AACxC,UAAI,YAAY,CAAC,MAAM,KAAM;AAC7B,UAAI,MAAM,aAAa;AACrB,6BAAqB,MAAM;AAAA,MAC7B;AACA,gBAAU,QAAQ,WAAW,MAAM,MAAM,MAAM,eAAe,MAAS;AAAA,IACzE,CAAC;AAED,eAAW,aAAa,cAAc;AACpC,SAAG,iBAAiB,WAAW,CAAC,UAAU;AACxC,YAAI,SAAU;AACd,kBAAU,QAAQ,WAAW,MAAM,QAAQ,IAAI,MAAS;AAAA,MAC1D,CAAC;AAAA,IACH;AAEA,OAAG,iBAAiB,SAAS,CAAC,UAAU;AACtC,UAAI,SAAU;AAEd,YAAM,MAAM,aAAa,QAAQ,OAAO,MAAM,OAAO,IAAI;AAEzD,UAAI,IAAI,SAAS,mBAAmB,KAAK,IAAI,SAAS,KAAK,GAAG;AAC5D,kBAAU,WAAW;AACrB,gBAAQ;AACR,mBAAW;AACX;AAAA,MACF;AAEA,UAAI,IAAK,WAAU,UAAU,IAAI,MAAM,GAAG,CAAC;AAE3C,wBAAkB;AAAA,IACpB,CAAC;AAED,OAAG,iBAAiB,SAAS,MAAM;AACjC,UAAI,SAAU;AACd,wBAAkB;AAAA,IACpB,CAAC;AAAA,EACH;AAEA,UAAQ;AAER,SAAO;AAAA,IACL,OAAO,MAAM;AACX,iBAAW;AACX,UAAI,gBAAgB;AAClB,qBAAa,cAAc;AAC3B,yBAAiB;AAAA,MACnB;AACA,cAAQ;AAAA,IACV;AAAA,EACF;AACF;;;AC1GA,IAAMA,8BAA6B;AACnC,IAAM,eAAe;AAmBrB,eAAsB,kBACpB,QACA,WACA,SACA,WACyB;AACzB,QAAM,aAAa,iBAAiB,OAAO,UAAU,SAAS;AAC9D,QAAM,gBAAgB,OAAO,WAAW,CAAC;AACzC,QAAM,iBAAiB,OAAO,oBAAoBA;AAElD,YAAU,eAAe,YAAY;AAErC,QAAM,UAAU,MAAM,MAAM,GAAG,UAAU,aAAa,SAAS,SAAS;AAAA,IACtE,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,cAAc;AAAA,IAChE,MAAM,KAAK,UAAU,OAAO;AAAA,EAC9B,CAAC;AAED,MAAI,QAAQ,WAAW,KAAK;AAC1B,UAAM,IAAI,mBAAmB,SAAS;AAAA,EACxC;AAEA,MAAI,CAAC,QAAQ,IAAI;AACf,UAAM,IAAI,MAAM,gBAAgB,QAAQ,MAAM,IAAI,MAAM,QAAQ,KAAK,CAAC,EAAE;AAAA,EAC1E;AAEA,QAAM,EAAE,UAAU,cAAc,UAAU,IAAqB,MAAM,QAAQ,KAAK;AAElF,MAAI,cACF,SAAS,SAAS,IAAI,SAAS,SAAS,SAAS,CAAC,EAAG,KAAK;AAE5D,QAAM,WAAW;AAEjB,QAAM,iBAAiB,IAAI,IAAY,SAAS,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAEhE,MAAI,UAAgC;AACpC,MAAI,WAAW;AAEf,WAAS,cAAoB;AAC3B,QAAI,eAAe,QAAQ,aAAc;AACzC,UAAM,MAAM,CAAC,GAAG,cAAc;AAC9B,mBAAe,MAAM;AACrB,eAAW,MAAM,IAAI,MAAM,CAAC,YAAY,GAAG;AACzC,qBAAe,IAAI,EAAE;AAAA,IACvB;AAAA,EACF;AAEA,WAAS,UAAgB;AACvB,QAAI,SAAU;AAEd,UAAM,YAAY,cACd,GAAG,UAAU,aAAa,SAAS,YACnC,GAAG,UAAU,aAAa,SAAS,sBAAsB,mBAAmB,QAAQ,CAAC;AAEzF,cAAU;AAAA,MACR;AAAA,QACE,KAAK;AAAA,QACL,SAAS;AAAA,QACT,kBAAkB;AAAA,QAClB;AAAA,QACA,cAAc,CAAC,QAAQ;AAAA,MACzB;AAAA,MACA;AAAA,QACE,QAAQ,MAAM;AACZ,cAAI,CAAC,SAAU,WAAU,eAAe,WAAW;AAAA,QACrD;AAAA,QACA,SAAS,CAAC,WAAW,MAAM,YAAY;AACrC,cAAI,SAAU;AAEd,cAAI,cAAc,WAAW;AAC3B,gBAAI;AACJ,gBAAI;AACF,wBAAU,KAAK,MAAM,IAAI;AAAA,YAC3B,QAAQ;AACN,wBAAU,QAAQ,IAAI,MAAM,6BAA6B,CAAC;AAC1D;AAAA,YACF;AAEA,gBAAI,SAAS;AACX,4BAAc;AAAA,YAChB;AAEA,gBAAI,eAAe,IAAI,QAAQ,EAAE,EAAG;AACpC,2BAAe,IAAI,QAAQ,EAAE;AAC7B,wBAAY;AAEZ,sBAAU,UAAU,OAAO;AAAA,UAC7B,WAAW,cAAc,UAAU;AACjC,qBAAS,MAAM;AACf,sBAAU;AACV,sBAAU,SAAS;AAAA,UACrB;AAAA,QACF;AAAA,QACA,SAAS,CAAC,QAAQ;AAChB,cAAI,CAAC,SAAU,WAAU,QAAQ,GAAG;AAAA,QACtC;AAAA,QACA,UAAU,MAAM;AACd,oBAAU,eAAe,QAAQ;AACjC,qBAAW;AAAA,QACb;AAAA,QACA,gBAAgB,MAAM;AACpB,cAAI,CAAC,SAAU,WAAU,eAAe,cAAc;AAAA,QACxD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,UAAQ;AAER,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,qBAAqB;AAAA,IACrB,iBAAiB;AAAA,IAEjB,aAAa,OAAO,MAAM,MAAM,eAAe;AAC7C,UAAI,SAAU,OAAM,IAAI,sBAAsB,cAAc;AAE5D,YAAM,UAAiC;AAAA,QACrC,WAAW,QAAQ;AAAA,QACnB;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAEA,YAAM,MAAM,MAAM;AAAA,QAChB,GAAG,UAAU,aAAa,SAAS;AAAA,QACnC;AAAA,UACE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,cAAc;AAAA,UAChE,MAAM,KAAK,UAAU,OAAO;AAAA,QAC9B;AAAA,MACF;AAEA,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI,MAAM,gBAAgB,IAAI,MAAM,IAAI,MAAM,IAAI,KAAK,CAAC,EAAE;AAAA,MAClE;AAEA,YAAM,WAAgC,MAAM,IAAI,KAAK;AACrD,qBAAe,IAAI,SAAS,EAAE;AAC9B,aAAO;AAAA,IACT;AAAA,IAEA,YAAY,OAAO,cAAsB;AACvC,YAAM,MAAM,MAAM,MAAM,GAAG,UAAU,aAAa,SAAS,SAAS;AAAA,QAClE,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,cAAc;AAAA,QAChE,MAAM,KAAK,UAAU;AAAA,UACnB,gBAAgB,QAAQ;AAAA,UACxB,YAAY;AAAA,QACd,CAAC;AAAA,MACH,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI,MAAM,sBAAsB,IAAI,MAAM,EAAE;AAAA,MACpD;AAAA,IACF;AAAA,IAEA,YAAY,MAAM;AAChB,iBAAW;AACX,eAAS,MAAM;AACf,gBAAU;AACV,gBAAU,eAAe,cAAc;AAAA,IACzC;AAAA,EACF;AACF;;;AJzLA,IAAM,8BAA8B;AAQ7B,SAAS,QACd,EAAE,QAAQ,WAAW,SAAS,UAAU,GACtB;AAClB,QAAM,CAAC,UAAU,WAAW,IAAI,SAAuB,CAAC,CAAC;AACzD,QAAM,CAAC,cAAc,eAAe,IAAI,SAA2B,CAAC,CAAC;AACrE,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAqB,YAAY;AAC7D,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAuB,IAAI;AAErD,QAAM,aAAa,OAA8B,IAAI;AACrD,QAAM,cAAc,OAAO,KAAK;AAChC,QAAM,cAAc,OAAO,QAAQ;AACnC,cAAY,UAAU;AACtB,QAAM,YAAY,OAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,QAAM,cAAc,OAAuB,SAAS,YAAY;AAChE,QAAM,qBAAqB,OAA6C,IAAI;AAC5E,QAAM,aAAa,OAAO,OAAO;AACjC,aAAW,UAAU;AACrB,QAAM,YAAY,OAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,QAAM,eAAe,OAAO,SAAS;AACrC,eAAa,UAAU;AACvB,QAAM,cAAc,OAAO,KAAK;AAChC,QAAM,uBAAuB,OAAO,oBAAI,IAAY,CAAC;AAErD,QAAM,oBACJ,OAAO,sBAAsB,SAAS,OAAO,YAAY,8BAA8B;AAEzF,QAAM,YAAiC;AAAA,IACrC,WAAW,CAAC,YAAY;AACtB,UAAI,YAAY,QAAS;AACzB,kBAAY,CAAC,SAAuB;AAElC,cAAM,gBAAgB,KAAK;AAAA,UACzB,CAAC,MACC,qBAAqB,QAAQ,IAAI,EAAE,EAAE,KACrC,EAAE,cAAc,QAAQ,aACxB,EAAE,SAAS,QAAQ,QACnB,EAAE,SAAS,QAAQ;AAAA,QACvB;AACA,YAAI,kBAAkB,IAAI;AACxB,gBAAM,eAAe,KAAK,aAAa,EAAG;AAC1C,+BAAqB,QAAQ,OAAO,YAAY;AAChD,gBAAM,OAAO,CAAC,GAAG,IAAI;AACrB,eAAK,aAAa,IAAI;AACtB,iBAAO;AAAA,QACT;AACA,eAAO,CAAC,GAAG,MAAM,OAAO;AAAA,MAC1B,CAAC;AACD,mBAAa,UAAU,OAAO;AAAA,IAChC;AAAA,IACA,gBAAgB,CAAC,eAAe;AAC9B,UAAI,YAAY,QAAS;AACzB,gBAAU,UAAU;AACpB,UAAI,eAAe,YAAa,UAAS,IAAI;AAAA,IAC/C;AAAA,IACA,SAAS,CAAC,QAAQ;AAChB,UAAI,YAAY,QAAS;AACzB,eAAS,GAAG;AAAA,IACd;AAAA,IACA,UAAU,MAAM;AACd,UAAI,YAAY,QAAS;AACzB,mBAAa;AAAA,IACf;AAAA,EACF;AAEA,iBAAe,eAA8B;AAC3C,QAAI,YAAY,QAAS;AACzB,gBAAY,UAAU;AAEtB,UAAM,WAAW,WAAW;AAC5B,QAAI,UAAU;AACZ,eAAS,WAAW;AACpB,iBAAW,UAAU;AAAA,IACvB;AAEA,QAAI;AACF,YAAM,UAAU,MAAM,kBAAqB,UAAU,SAAS,WAAW,WAAW,SAAS,SAAS;AAEtG,UAAI,YAAY,SAAS;AACvB,gBAAQ,WAAW;AACnB;AAAA,MACF;AAEA,iBAAW,UAAU;AACrB,sBAAgB,QAAQ,mBAAmB;AAC3C,kBAAY,QAAQ,eAAe;AAAA,IACrC,SAAS,KAAK;AACZ,UAAI,YAAY,QAAS;AAEzB,UAAI,eAAe,oBAAoB;AACrC,kBAAU,QAAQ;AAClB,iBAAS,GAAG;AACZ;AAAA,MACF;AAEA,gBAAU,OAAO;AACjB,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAC9D,UAAE;AACA,kBAAY,UAAU;AAAA,IACxB;AAAA,EACF;AAEA,YAAU,MAAM;AACd,gBAAY,UAAU;AACtB,iBAAa;AAEb,WAAO,MAAM;AACX,kBAAY,UAAU;AACtB,UAAI,UAAU,YAAY,eAAe,WAAW,SAAS;AAC3D,cAAM,UAAU,YAAY,QAAQ,YAAY,QAAQ,SAAS,CAAC;AAClE,YAAI,SAAS;AACX,qBAAW,QAAQ,WAAW,QAAQ,EAAE,EAAE,MAAM,MAAM;AAAA,UAAC,CAAC;AAAA,QAC1D;AAAA,MACF;AACA,UAAI,mBAAmB,SAAS;AAC9B,qBAAa,mBAAmB,OAAO;AACvC,2BAAmB,UAAU;AAAA,MAC/B;AACA,iBAAW,SAAS,WAAW;AAC/B,iBAAW,UAAU;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,SAAS,CAAC;AAEd,YAAU,MAAM;AACd,aAAS,kBAAwB;AAC/B,iBAAW,SAAS,WAAW;AAC/B,iBAAW,UAAU;AACrB,gBAAU,cAAc;AAAA,IAC1B;AAEA,UAAM,eAAe,SAAS,iBAAiB,UAAU,CAAC,iBAAiB;AACzE,YAAM,OAAO,YAAY;AACzB,kBAAY,UAAU;AAEtB,UAAI,CAAC,WAAW,WAAW,iBAAiB,SAAU;AAEtD,YAAM,iBACJ,iBAAiB,gBAChB,SAAS,OAAO,SAAS,iBAAiB;AAE7C,UAAI,iBAAiB,UAAU;AAC7B,YAAI,mBAAmB,SAAS;AAC9B,uBAAa,mBAAmB,OAAO;AACvC,6BAAmB,UAAU;AAC7B;AAAA,QACF;AAEA,YAAI,KAAK,MAAM,qBAAqB,KAAK,CAAC,WAAW,SAAS;AAC5D,uBAAa;AAAA,QACf;AAAA,MACF,WAAW,gBAAgB;AACzB,YAAI,mBAAmB,QAAS;AAEhC,YAAI,sBAAsB,GAAG;AAC3B,0BAAgB;AAAA,QAClB,OAAO;AACL,6BAAmB,UAAU,WAAW,MAAM;AAC5C,+BAAmB,UAAU;AAC7B,4BAAgB;AAAA,UAClB,GAAG,iBAAiB;AAAA,QACtB;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO,MAAM;AACX,mBAAa,OAAO;AACpB,UAAI,mBAAmB,SAAS;AAC9B,qBAAa,mBAAmB,OAAO;AACvC,2BAAmB,UAAU;AAAA,MAC/B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,WAAW,iBAAiB,CAAC;AAEjC,QAAM,cAAc;AAAA,IAClB,OAAO,MAAwB,MAAc,eAAoE;AAC/G,YAAM,UAAU,WAAW;AAC3B,UAAI,CAAC,QAAS,OAAM,IAAI,sBAAsB,UAAU,OAAO;AAE/D,YAAM,aAAa,UAAU,QAAQ,mBAAmB;AACxD,UAAI,eAA8B;AAElC,UAAI,YAAY;AACd,uBAAe,cAAc,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC;AACjF,6BAAqB,QAAQ,IAAI,YAAY;AAC7C,cAAM,iBAA6B;AAAA,UACjC,IAAI;AAAA,UACJ,YAAY;AAAA,UACZ,WAAW,WAAW,QAAQ;AAAA,UAC9B,aAAa,WAAW,QAAQ;AAAA,UAChC;AAAA,UACA;AAAA,UACA,YAAa,cAAc,CAAC;AAAA,UAC5B,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,QACrC;AACA,oBAAY,CAAC,SAAS,CAAC,GAAG,MAAM,cAAc,CAAC;AAAA,MACjD;AAEA,UAAI;AACF,cAAM,WAAW,MAAM,QAAQ,YAAY,MAAM,MAAM,UAAU;AAEjE,YAAI,cAAc,cAAc;AAC9B,+BAAqB,QAAQ,OAAO,YAAY;AAChD,sBAAY,CAAC,SAAS;AAEpB,kBAAM,eAAe,KAAK,KAAK,CAAC,MAAM,EAAE,OAAO,YAAY;AAC3D,gBAAI,CAAC,aAAc,QAAO;AAC1B,mBAAO,KAAK;AAAA,cAAI,CAAC,MACf,EAAE,OAAO,eACL,EAAE,GAAG,GAAG,IAAI,SAAS,IAAI,YAAY,SAAS,WAAW,IACzD;AAAA,YACN;AAAA,UACF,CAAC;AAAA,QACH;AAEA,eAAO;AAAA,MACT,SAAS,KAAK;AACZ,YAAI,cAAc,cAAc;AAC9B,+BAAqB,QAAQ,OAAO,YAAY;AAChD,sBAAY,CAAC,SAAS,KAAK,OAAO,CAAC,MAAM,EAAE,OAAO,YAAY,CAAC;AAAA,QACjE;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,CAAC,SAAS;AAAA,EACZ;AAEA,QAAM,aAAa,YAAY,MAAM;AACnC,eAAW,SAAS,WAAW;AAC/B,eAAW,UAAU;AACrB,cAAU,cAAc;AAAA,EAC1B,GAAG,CAAC,CAAC;AAEL,SAAO,EAAE,UAAU,cAAc,QAAQ,OAAO,aAAa,WAAW;AAC1E;;;AKhQA,SAAS,aAAAC,YAAW,UAAAC,SAAQ,YAAAC,WAAU,eAAAC,oBAAmB;AACzD,SAAS,YAAAC,WAAU,YAAAC,iBAAqC;AAMxD,IAAMC,+BAA8B;AACpC,IAAM,uBAAuB,CAAC,mBAAmB,iBAAiB,cAAc;AAgBzE,SAAS,UAAU,SAA4C;AACpE,QAAM,EAAE,QAAQ,WAAW,eAAe,UAAU,KAAK,IAAI;AAE7D,QAAM,CAAC,aAAa,cAAc,IAAIC,UAAS,CAAC;AAChD,QAAM,CAAC,eAAe,gBAAgB,IAAIA,UAAwB,IAAI;AACtE,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAuB,IAAI;AAErD,QAAM,UAAUC,QAA6B,IAAI;AACjD,QAAM,YAAYA,QAAO,MAAM;AAC/B,YAAU,UAAU;AACpB,QAAM,cAAcA,QAAuBC,UAAS,YAAY;AAChE,QAAM,qBAAqBD,QAA6C,IAAI;AAE5E,QAAM,oBACJ,OAAO,sBAAsBE,UAAS,OAAO,YAAYJ,+BAA8B;AAEzF,QAAM,UAAUK,aAAY,MAAM;AAChC,YAAQ,SAAS,MAAM;AACvB,YAAQ,UAAU;AAElB,UAAM,aAAa,iBAAiB,UAAU,QAAQ,UAAU,SAAS;AACzE,UAAM,gBAAgB,UAAU,QAAQ,WAAW,CAAC;AACpD,UAAM,MAAM,GAAG,UAAU,aAAa,SAAS,0BAA0B,mBAAmB,aAAa,CAAC;AAE1G,YAAQ,UAAU;AAAA,MAChB;AAAA,QACE;AAAA,QACA,SAAS;AAAA,QACT,kBAAkB,UAAU,QAAQ;AAAA,QACpC,cAAc;AAAA,MAChB;AAAA,MACA;AAAA,QACE,QAAQ,MAAM;AACZ,mBAAS,IAAI;AAAA,QACf;AAAA,QACA,SAAS,CAAC,WAAW,SAAS;AAC5B,cAAI;AACF,gBAAI,cAAc,mBAAmB;AACnC,oBAAM,WAAgC,KAAK,MAAM,IAAI;AACrD,6BAAe,SAAS,YAAY;AACpC,+BAAiB,SAAS,eAAe;AAAA,YAC3C,WAAW,cAAc,iBAAiB;AACxC,oBAAM,SAAuC,KAAK,MAAM,IAAI;AAC5D,6BAAe,CAAC,SAAS,OAAO,CAAC;AACjC,+BAAiB,OAAO,UAAU;AAAA,YACpC,WAAW,cAAc,gBAAgB;AACvC,oBAAM,QAAsD,KAAK,MAAM,IAAI;AAC3E,6BAAe,MAAM,YAAY;AAAA,YACnC;AAAA,UACF,QAAQ;AACN,qBAAS,IAAI,MAAM,kCAAkC,CAAC;AAAA,UACxD;AAAA,QACF;AAAA,QACA,SAAS,CAAC,QAAQ;AAChB,mBAAS,GAAG;AAAA,QACd;AAAA,QACA,UAAU,MAAM;AACd,kBAAQ,UAAU;AAAA,QACpB;AAAA,MACF;AAAA,IACF;AAAA,EACF,GAAG,CAAC,WAAW,aAAa,CAAC;AAE7B,QAAM,aAAaA,aAAY,MAAM;AACnC,YAAQ,SAAS,MAAM;AACvB,YAAQ,UAAU;AAAA,EACpB,GAAG,CAAC,CAAC;AAEL,EAAAC,WAAU,MAAM;AACd,mBAAe,CAAC;AAChB,qBAAiB,IAAI;AACrB,aAAS,IAAI;AAEb,QAAI,CAAC,SAAS;AACZ,iBAAW;AACX;AAAA,IACF;AAEA,YAAQ;AAER,WAAO,MAAM;AACX,iBAAW;AACX,UAAI,mBAAmB,SAAS;AAC9B,qBAAa,mBAAmB,OAAO;AACvC,2BAAmB,UAAU;AAAA,MAC/B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,WAAW,eAAe,SAAS,SAAS,UAAU,CAAC;AAE3D,EAAAA,WAAU,MAAM;AACd,QAAI,CAAC,QAAS;AAEd,UAAM,eAAeH,UAAS,iBAAiB,UAAU,CAAC,iBAAiB;AACzE,YAAM,OAAO,YAAY;AACzB,kBAAY,UAAU;AAEtB,UAAI,CAAC,QAAQ,WAAW,iBAAiB,SAAU;AAEnD,YAAM,iBACJ,iBAAiB,gBAChBC,UAAS,OAAO,SAAS,iBAAiB;AAE7C,UAAI,iBAAiB,UAAU;AAC7B,YAAI,mBAAmB,SAAS;AAC9B,uBAAa,mBAAmB,OAAO;AACvC,6BAAmB,UAAU;AAC7B;AAAA,QACF;AAEA,YAAI,KAAK,MAAM,qBAAqB,KAAK,CAAC,QAAQ,SAAS;AACzD,kBAAQ;AAAA,QACV;AAAA,MACF,WAAW,gBAAgB;AACzB,YAAI,mBAAmB,QAAS;AAEhC,YAAI,sBAAsB,GAAG;AAC3B,qBAAW;AAAA,QACb,OAAO;AACL,6BAAmB,UAAU,WAAW,MAAM;AAC5C,+BAAmB,UAAU;AAC7B,uBAAW;AAAA,UACb,GAAG,iBAAiB;AAAA,QACtB;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO,MAAM;AACX,mBAAa,OAAO;AACpB,UAAI,mBAAmB,SAAS;AAC9B,qBAAa,mBAAmB,OAAO;AACvC,2BAAmB,UAAU;AAAA,MAC/B;AAAA,IACF;AAAA,EACF,GAAG,CAAC,SAAS,mBAAmB,SAAS,UAAU,CAAC;AAEpD,SAAO,EAAE,aAAa,WAAW,cAAc,GAAG,eAAe,MAAM;AACzE;","names":["DEFAULT_RECONNECT_DELAY_MS","useEffect","useRef","useState","useCallback","AppState","Platform","DEFAULT_BACKGROUND_GRACE_MS","useState","useRef","AppState","Platform","useCallback","useEffect"]}
|
|
1
|
+
{"version":3,"sources":["../src/use-chat.ts","../src/errors.ts","../src/resolve-url.ts","../src/sse-connection.ts","../src/session.ts","../src/use-unread.ts"],"sourcesContent":["import { useEffect, useRef, useState, useCallback } from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport type {\n ChatDomain,\n DefaultDomain,\n Message,\n Participant,\n MessageAttributes,\n SendMessageResponse,\n} from '@pedi/chika-types';\nimport type { UseChatOptions, UseChatReturn, ChatStatus } from './types';\nimport { ChatDisconnectedError, ChannelClosedError } from './errors';\nimport { createChatSession, type ChatSession, type SessionCallbacks } from './session';\n\nconst DEFAULT_BACKGROUND_GRACE_MS = 2000;\n\n/**\n * React hook for real-time chat over SSE.\n * Manages connection lifecycle, AppState transitions, message deduplication, and reconnection.\n *\n * @template D - Chat domain type for role/message type narrowing. Defaults to DefaultDomain.\n */\nexport function useChat<D extends ChatDomain = DefaultDomain>(\n { config, channelId, profile, onMessage }: UseChatOptions<D>,\n): UseChatReturn<D> {\n const [messages, setMessages] = useState<Message<D>[]>([]);\n const [participants, setParticipants] = useState<Participant<D>[]>([]);\n const [status, setStatus] = useState<ChatStatus>('connecting');\n const [error, setError] = useState<Error | null>(null);\n\n const sessionRef = useRef<ChatSession<D> | null>(null);\n const disposedRef = useRef(false);\n const messagesRef = useRef(messages);\n messagesRef.current = messages;\n const statusRef = useRef(status);\n statusRef.current = status;\n const appStateRef = useRef<AppStateStatus>(AppState.currentState);\n const backgroundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const profileRef = useRef(profile);\n profileRef.current = profile;\n const configRef = useRef(config);\n configRef.current = config;\n const onMessageRef = useRef(onMessage);\n onMessageRef.current = onMessage;\n const startingRef = useRef(false);\n const pendingOptimisticIds = useRef(new Set<string>());\n\n const backgroundGraceMs =\n config.backgroundGraceMs ?? (Platform.OS === 'android' ? DEFAULT_BACKGROUND_GRACE_MS : 0);\n\n const callbacks: SessionCallbacks<D> = {\n onMessage: (message) => {\n if (disposedRef.current) return;\n setMessages((prev: Message<D>[]) => {\n // Fast path: no pending optimistic messages, just append.\n if (pendingOptimisticIds.current.size === 0) {\n return [...prev, message];\n }\n\n // Slow path: check if this SSE message reconciles a pending optimistic message.\n const optimisticIdx = prev.findIndex(\n (m) =>\n pendingOptimisticIds.current.has(m.id) &&\n m.sender_id === message.sender_id &&\n m.body === message.body &&\n m.type === message.type,\n );\n if (optimisticIdx !== -1) {\n const optimisticId = prev[optimisticIdx]!.id;\n pendingOptimisticIds.current.delete(optimisticId);\n const next = [...prev];\n next[optimisticIdx] = message;\n return next;\n }\n return [...prev, message];\n });\n onMessageRef.current?.(message);\n },\n onStatusChange: (nextStatus) => {\n if (disposedRef.current) return;\n setStatus(nextStatus);\n if (nextStatus === 'connected') setError(null);\n },\n onError: (err) => {\n if (disposedRef.current) return;\n setError(err);\n },\n onResync: () => {\n if (disposedRef.current) return;\n startSession();\n },\n };\n\n async function startSession(): Promise<void> {\n if (startingRef.current) return;\n startingRef.current = true;\n\n const existing = sessionRef.current;\n if (existing) {\n existing.disconnect();\n sessionRef.current = null;\n }\n\n try {\n const session = await createChatSession<D>(configRef.current, channelId, profileRef.current, callbacks);\n\n if (disposedRef.current) {\n session.disconnect();\n return;\n }\n\n sessionRef.current = session;\n setParticipants(session.initialParticipants);\n setMessages(session.initialMessages);\n } catch (err) {\n if (disposedRef.current) return;\n\n if (err instanceof ChannelClosedError) {\n setStatus('closed');\n setError(err);\n return;\n }\n\n setStatus('error');\n setError(err instanceof Error ? err : new Error(String(err)));\n } finally {\n startingRef.current = false;\n }\n }\n\n useEffect(() => {\n disposedRef.current = false;\n startSession();\n\n return () => {\n disposedRef.current = true;\n if (statusRef.current === 'connected' && sessionRef.current) {\n const lastMsg = messagesRef.current[messagesRef.current.length - 1];\n if (lastMsg) {\n sessionRef.current.markAsRead(lastMsg.id).catch(() => {});\n }\n }\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n };\n }, [channelId]);\n\n useEffect(() => {\n function teardownSession(): void {\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n setStatus('disconnected');\n }\n\n const subscription = AppState.addEventListener('change', (nextAppState) => {\n const prev = appStateRef.current;\n appStateRef.current = nextAppState;\n\n if (!sessionRef.current && nextAppState !== 'active') return;\n\n const shouldTeardown =\n nextAppState === 'background' ||\n (Platform.OS === 'ios' && nextAppState === 'inactive');\n\n if (nextAppState === 'active') {\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n return;\n }\n\n if (prev.match(/inactive|background/) && !sessionRef.current) {\n startSession();\n }\n } else if (shouldTeardown) {\n if (backgroundTimerRef.current) return;\n\n if (backgroundGraceMs === 0) {\n teardownSession();\n } else {\n backgroundTimerRef.current = setTimeout(() => {\n backgroundTimerRef.current = null;\n teardownSession();\n }, backgroundGraceMs);\n }\n }\n });\n\n return () => {\n subscription.remove();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [channelId, backgroundGraceMs]);\n\n const sendMessage = useCallback(\n async (type: D['messageType'], body: string, attributes?: MessageAttributes<D>): Promise<SendMessageResponse> => {\n const session = sessionRef.current;\n if (!session) throw new ChatDisconnectedError(statusRef.current);\n\n const optimistic = configRef.current.optimisticSend !== false;\n let optimisticId: string | null = null;\n\n if (optimistic) {\n optimisticId = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;\n pendingOptimisticIds.current.add(optimisticId);\n const provisionalMsg: Message<D> = {\n id: optimisticId,\n channel_id: channelId,\n sender_id: profileRef.current.id,\n sender_role: profileRef.current.role as D['role'],\n type,\n body,\n attributes: (attributes ?? {}) as MessageAttributes<D>,\n created_at: new Date().toISOString(),\n };\n setMessages((prev) => [...prev, provisionalMsg]);\n }\n\n try {\n const response = await session.sendMessage(type, body, attributes);\n\n if (optimistic && optimisticId) {\n pendingOptimisticIds.current.delete(optimisticId);\n setMessages((prev) => {\n // If SSE already reconciled this message, the optimistic ID is gone.\n const stillPending = prev.some((m) => m.id === optimisticId);\n if (!stillPending) return prev;\n return prev.map((m) =>\n m.id === optimisticId\n ? { ...m, id: response.id, created_at: response.created_at }\n : m,\n );\n });\n }\n\n return response;\n } catch (err) {\n if (optimistic && optimisticId) {\n pendingOptimisticIds.current.delete(optimisticId);\n setMessages((prev) => prev.filter((m) => m.id !== optimisticId));\n }\n throw err;\n }\n },\n [channelId],\n );\n\n const disconnect = useCallback(() => {\n sessionRef.current?.disconnect();\n sessionRef.current = null;\n setStatus('disconnected');\n }, []);\n\n return { messages, participants, status, error, sendMessage, disconnect };\n}\n","import type { ChatStatus } from './types';\n\nexport class ChatDisconnectedError extends Error {\n constructor(public readonly status: ChatStatus) {\n super(`Cannot send message while ${status}`);\n this.name = 'ChatDisconnectedError';\n }\n}\n\nexport class ChannelClosedError extends Error {\n constructor(public readonly channelId: string) {\n super(`Channel ${channelId} is closed`);\n this.name = 'ChannelClosedError';\n }\n}\n","import type { ChatManifest } from '@pedi/chika-types';\n\n/**\n * Creates a single-server manifest. Use this when all channels route to the same server.\n *\n * @example\n * ```ts\n * const config: ChatConfig = { manifest: createManifest('https://chat.example.com') };\n * ```\n */\nexport function createManifest(serverUrl: string): ChatManifest {\n return { buckets: [{ group: 'default', range: [0, 99], server_url: serverUrl }] };\n}\n\nexport function resolveServerUrl(manifest: ChatManifest, channelId: string): string {\n const hash = [...channelId].reduce((sum, c) => sum + c.charCodeAt(0), 0) % 100;\n const bucket = manifest.buckets.find(\n (b) => hash >= b.range[0] && hash <= b.range[1],\n );\n if (!bucket) throw new Error(`No chat bucket for hash ${hash}`);\n return bucket.server_url;\n}\n","import EventSource from 'react-native-sse';\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\n\nexport interface SSEConnectionConfig {\n url: string;\n headers?: Record<string, string>;\n reconnectDelayMs?: number;\n lastEventId?: string;\n customEvents?: string[];\n}\n\nexport interface SSEConnectionCallbacks {\n onOpen?: () => void;\n onEvent: (eventType: string, data: string, lastEventId?: string) => void;\n onError?: (error: Error) => void;\n onClosed?: () => void;\n onReconnecting?: () => void;\n}\n\nexport interface SSEConnection {\n close: () => void;\n}\n\nexport function createSSEConnection(\n config: SSEConnectionConfig,\n callbacks: SSEConnectionCallbacks,\n): SSEConnection {\n const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;\n const customEvents = config.customEvents ?? [];\n\n let currentLastEventId = config.lastEventId;\n let es: EventSource<string> | null = null;\n let disposed = false;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n function cleanup(): void {\n if (es) {\n es.removeAllEventListeners();\n es.close();\n es = null;\n }\n }\n\n function scheduleReconnect(): void {\n if (disposed || reconnectTimer) return;\n callbacks.onReconnecting?.();\n cleanup();\n\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n connect();\n }, reconnectDelay);\n }\n\n function connect(): void {\n if (disposed) return;\n\n es = new EventSource<string>(config.url, {\n headers: {\n ...config.headers,\n ...(currentLastEventId && { 'Last-Event-ID': currentLastEventId }),\n },\n pollingInterval: 0,\n });\n\n es.addEventListener('open', () => {\n if (disposed) return;\n callbacks.onOpen?.();\n });\n\n es.addEventListener('message', (event) => {\n if (disposed || !event.data) return;\n if (event.lastEventId) {\n currentLastEventId = event.lastEventId;\n }\n callbacks.onEvent('message', event.data, event.lastEventId ?? undefined);\n });\n\n for (const eventName of customEvents) {\n es.addEventListener(eventName, (event) => {\n if (disposed) return;\n callbacks.onEvent(eventName, event.data ?? '', undefined);\n });\n }\n\n es.addEventListener('error', (event) => {\n if (disposed) return;\n\n const msg = 'message' in event ? String(event.message) : '';\n\n if (msg.includes('Channel is closed') || msg.includes('410')) {\n callbacks.onClosed?.();\n cleanup();\n disposed = true;\n return;\n }\n\n if (msg) callbacks.onError?.(new Error(msg));\n\n scheduleReconnect();\n });\n\n es.addEventListener('close', () => {\n if (disposed) return;\n scheduleReconnect();\n });\n }\n\n connect();\n\n return {\n close: () => {\n disposed = true;\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n cleanup();\n },\n };\n}\n","import type {\n ChatDomain,\n DefaultDomain,\n Participant,\n Message,\n JoinResponse,\n SendMessageRequest,\n SendMessageResponse,\n MessageAttributes,\n} from '@pedi/chika-types';\nimport type { ChatConfig, ChatStatus } from './types';\nimport { ChatDisconnectedError, ChannelClosedError } from './errors';\nimport { resolveServerUrl } from './resolve-url';\nimport { createSSEConnection, type SSEConnection } from './sse-connection';\n\nconst DEFAULT_RECONNECT_DELAY_MS = 3000;\nconst MAX_SEEN_IDS = 500;\n\nexport interface SessionCallbacks<D extends ChatDomain = DefaultDomain> {\n onMessage: (message: Message<D>) => void;\n onStatusChange: (status: ChatStatus) => void;\n onError: (error: Error) => void;\n onResync: () => void;\n}\n\nexport interface ChatSession<D extends ChatDomain = DefaultDomain> {\n serviceUrl: string;\n channelId: string;\n initialParticipants: Participant<D>[];\n initialMessages: Message<D>[];\n sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;\n markAsRead: (messageId: string) => Promise<void>;\n disconnect: () => void;\n}\n\nexport async function createChatSession<D extends ChatDomain = DefaultDomain>(\n config: ChatConfig,\n channelId: string,\n profile: Participant<D>,\n callbacks: SessionCallbacks<D>,\n): Promise<ChatSession<D>> {\n const serviceUrl = resolveServerUrl(config.manifest, channelId);\n const customHeaders = config.headers ?? {};\n const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;\n\n callbacks.onStatusChange('connecting');\n\n const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(profile),\n });\n\n if (joinRes.status === 410) {\n throw new ChannelClosedError(channelId);\n }\n\n if (!joinRes.ok) {\n throw new Error(`Join failed: ${joinRes.status} ${await joinRes.text()}`);\n }\n\n const { messages, participants, joined_at }: JoinResponse<D> = await joinRes.json();\n\n let lastEventId =\n messages.length > 0 ? messages[messages.length - 1]!.id : undefined;\n\n const joinedAt = joined_at;\n\n const seenMessageIds = new Set<string>(messages.map((m) => m.id));\n\n let sseConn: SSEConnection | null = null;\n let disposed = false;\n\n const TRIM_THRESHOLD = MAX_SEEN_IDS * 1.5;\n\n function trimSeenIds(): void {\n if (seenMessageIds.size <= TRIM_THRESHOLD) return;\n const ids = [...seenMessageIds];\n seenMessageIds.clear();\n for (const id of ids.slice(-MAX_SEEN_IDS)) {\n seenMessageIds.add(id);\n }\n }\n\n function connect(): void {\n if (disposed) return;\n\n const streamUrl = lastEventId\n ? `${serviceUrl}/channels/${channelId}/stream`\n : `${serviceUrl}/channels/${channelId}/stream?since_time=${encodeURIComponent(joinedAt)}`;\n\n sseConn = createSSEConnection(\n {\n url: streamUrl,\n headers: customHeaders,\n reconnectDelayMs: reconnectDelay,\n lastEventId,\n customEvents: ['resync'],\n },\n {\n onOpen: () => {\n if (!disposed) callbacks.onStatusChange('connected');\n },\n onEvent: (eventType, data, eventId) => {\n if (disposed) return;\n\n if (eventType === 'message') {\n let message: Message<D>;\n try {\n message = JSON.parse(data);\n } catch {\n callbacks.onError(new Error('Failed to parse SSE message'));\n return;\n }\n\n if (eventId) {\n lastEventId = eventId;\n }\n\n if (seenMessageIds.has(message.id)) return;\n seenMessageIds.add(message.id);\n trimSeenIds();\n\n callbacks.onMessage(message);\n } else if (eventType === 'resync') {\n sseConn?.close();\n sseConn = null;\n callbacks.onResync();\n }\n },\n onError: (err) => {\n if (!disposed) callbacks.onError(err);\n },\n onClosed: () => {\n callbacks.onStatusChange('closed');\n disposed = true;\n },\n onReconnecting: () => {\n if (!disposed) callbacks.onStatusChange('reconnecting');\n },\n },\n );\n }\n\n connect();\n\n return {\n serviceUrl,\n channelId,\n initialParticipants: participants,\n initialMessages: messages,\n\n sendMessage: async (type, body, attributes) => {\n if (disposed) throw new ChatDisconnectedError('disconnected');\n\n const payload: SendMessageRequest<D> = {\n sender_id: profile.id,\n type,\n body,\n attributes,\n };\n\n const res = await fetch(\n `${serviceUrl}/channels/${channelId}/messages`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify(payload),\n },\n );\n\n if (!res.ok) {\n throw new Error(`Send failed: ${res.status} ${await res.text()}`);\n }\n\n const response: SendMessageResponse = await res.json();\n seenMessageIds.add(response.id);\n return response;\n },\n\n markAsRead: async (messageId: string) => {\n const res = await fetch(`${serviceUrl}/channels/${channelId}/read`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', ...customHeaders },\n body: JSON.stringify({\n participant_id: profile.id,\n message_id: messageId,\n }),\n });\n if (!res.ok) {\n throw new Error(`markAsRead failed: ${res.status}`);\n }\n },\n\n disconnect: () => {\n disposed = true;\n sseConn?.close();\n sseConn = null;\n callbacks.onStatusChange('disconnected');\n },\n };\n}\n","import { useEffect, useRef, useState, useCallback } from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport type { UnreadCountResponse, SSEUnreadUpdateEvent } from '@pedi/chika-types';\nimport type { ChatConfig } from './types';\nimport { resolveServerUrl } from './resolve-url';\nimport { createSSEConnection, type SSEConnection } from './sse-connection';\n\nconst DEFAULT_BACKGROUND_GRACE_MS = 2000;\nconst UNREAD_CUSTOM_EVENTS = ['unread_snapshot', 'unread_update', 'unread_clear'];\n\nexport interface UseUnreadOptions {\n config: ChatConfig;\n channelId: string;\n participantId: string;\n enabled?: boolean;\n}\n\nexport interface UseUnreadReturn {\n unreadCount: number;\n hasUnread: boolean;\n lastMessageAt: string | null;\n error: Error | null;\n}\n\nexport function useUnread(options: UseUnreadOptions): UseUnreadReturn {\n const { config, channelId, participantId, enabled = true } = options;\n\n const [unreadCount, setUnreadCount] = useState(0);\n const [lastMessageAt, setLastMessageAt] = useState<string | null>(null);\n const [error, setError] = useState<Error | null>(null);\n\n const connRef = useRef<SSEConnection | null>(null);\n const configRef = useRef(config);\n configRef.current = config;\n const appStateRef = useRef<AppStateStatus>(AppState.currentState);\n const backgroundTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n const backgroundGraceMs =\n config.backgroundGraceMs ?? (Platform.OS === 'android' ? DEFAULT_BACKGROUND_GRACE_MS : 0);\n\n const connect = useCallback(() => {\n connRef.current?.close();\n connRef.current = null;\n\n const serviceUrl = resolveServerUrl(configRef.current.manifest, channelId);\n const customHeaders = configRef.current.headers ?? {};\n const url = `${serviceUrl}/channels/${channelId}/unread?participant_id=${encodeURIComponent(participantId)}`;\n\n connRef.current = createSSEConnection(\n {\n url,\n headers: customHeaders,\n reconnectDelayMs: configRef.current.reconnectDelayMs,\n customEvents: UNREAD_CUSTOM_EVENTS,\n },\n {\n onOpen: () => {\n setError(null);\n },\n onEvent: (eventType, data) => {\n try {\n if (eventType === 'unread_snapshot') {\n const snapshot: UnreadCountResponse = JSON.parse(data);\n setUnreadCount(snapshot.unread_count);\n setLastMessageAt(snapshot.last_message_at);\n } else if (eventType === 'unread_update') {\n const update: SSEUnreadUpdateEvent['data'] = JSON.parse(data);\n setUnreadCount((prev) => prev + 1);\n setLastMessageAt(update.created_at);\n } else if (eventType === 'unread_clear') {\n const clear: { channel_id: string; unread_count: number } = JSON.parse(data);\n setUnreadCount(clear.unread_count);\n }\n } catch {\n setError(new Error('Failed to parse unread SSE event'));\n }\n },\n onError: (err) => {\n setError(err);\n },\n onClosed: () => {\n connRef.current = null;\n },\n },\n );\n }, [channelId, participantId]);\n\n const disconnect = useCallback(() => {\n connRef.current?.close();\n connRef.current = null;\n }, []);\n\n useEffect(() => {\n setUnreadCount(0);\n setLastMessageAt(null);\n setError(null);\n\n if (!enabled) {\n disconnect();\n return;\n }\n\n connect();\n\n return () => {\n disconnect();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [channelId, participantId, enabled, connect, disconnect]);\n\n useEffect(() => {\n if (!enabled) return;\n\n const subscription = AppState.addEventListener('change', (nextAppState) => {\n const prev = appStateRef.current;\n appStateRef.current = nextAppState;\n\n if (!connRef.current && nextAppState !== 'active') return;\n\n const shouldTeardown =\n nextAppState === 'background' ||\n (Platform.OS === 'ios' && nextAppState === 'inactive');\n\n if (nextAppState === 'active') {\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n return;\n }\n\n if (prev.match(/inactive|background/) && !connRef.current) {\n connect();\n }\n } else if (shouldTeardown) {\n if (backgroundTimerRef.current) return;\n\n if (backgroundGraceMs === 0) {\n disconnect();\n } else {\n backgroundTimerRef.current = setTimeout(() => {\n backgroundTimerRef.current = null;\n disconnect();\n }, backgroundGraceMs);\n }\n }\n });\n\n return () => {\n subscription.remove();\n if (backgroundTimerRef.current) {\n clearTimeout(backgroundTimerRef.current);\n backgroundTimerRef.current = null;\n }\n };\n }, [enabled, backgroundGraceMs, connect, disconnect]);\n\n return { unreadCount, hasUnread: unreadCount > 0, lastMessageAt, error };\n}\n"],"mappings":"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","names":["DEFAULT_RECONNECT_DELAY_MS","useEffect","useRef","useState","useCallback","AppState","Platform","DEFAULT_BACKGROUND_GRACE_MS","useState","useRef","AppState","Platform","useCallback","useEffect"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pedi/chika-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "React Native SDK for Pedi Chika chat service",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"typecheck": "tsc --noEmit"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@pedi/chika-types": "^1.0.
|
|
36
|
+
"@pedi/chika-types": "^1.0.5",
|
|
37
37
|
"react-native-sse": "^1.2.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|