@pedi/chika-sdk 1.0.2 → 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.d.mts +36 -8
- package/dist/index.d.ts +36 -8
- package/dist/index.js +282 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +279 -74
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -2
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/use-chat.ts","../src/errors.ts","../src/session.ts","../src/resolve-url.ts"],"sourcesContent":["export { useChat } from './use-chat';\nexport { createChatSession } from './session';\nexport { resolveServerUrl, createManifest } from './resolve-url';\nexport { ChatDisconnectedError, ChannelClosedError } from './errors';\n\nexport type { ChatConfig, ChatStatus, UseChatOptions, UseChatReturn } from './types';\nexport type { ChatSession, SessionCallbacks } from './session';\n\nexport type {\n ChatDomain,\n DefaultDomain,\n Message,\n Participant,\n MessageAttributes,\n SendMessageResponse,\n ChatManifest,\n ChatBucket,\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 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 (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 EventSource from 'react-native-sse';\nimport 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';\n\n// Custom SSE event types beyond built-in message/open/error/close.\n// 'heartbeat' is server keep-alive (no handler needed).\ntype ChatEvents = 'heartbeat' | 'resync';\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 disconnect: () => void;\n}\n\n/**\n * Creates an imperative chat session with SSE streaming and managed reconnection.\n * Lower-level API — prefer `useChat` hook for React Native components.\n *\n * @template D - Chat domain type. Defaults to DefaultDomain.\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 es: EventSource<ChatEvents> | null = null;\n let disposed = false;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\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 es = new EventSource<ChatEvents>(streamUrl, {\n headers: {\n ...customHeaders,\n ...(lastEventId && { 'Last-Event-ID': lastEventId }),\n },\n pollingInterval: 0,\n });\n\n es.addEventListener('open', () => {\n if (disposed) return;\n callbacks.onStatusChange('connected');\n });\n\n es.addEventListener('message', (event) => {\n if (disposed || !event.data) return;\n\n let message: Message<D>;\n try {\n message = JSON.parse(event.data);\n } catch {\n callbacks.onError(new Error('Failed to parse SSE message'));\n return;\n }\n\n if (event.lastEventId) {\n lastEventId = event.lastEventId;\n }\n\n if (seenMessageIds.has(message.id)) return;\n seenMessageIds.add(message.id);\n trimSeenIds();\n\n callbacks.onMessage(message);\n });\n\n es.addEventListener('resync', () => {\n if (disposed) return;\n cleanupEventSource();\n callbacks.onResync();\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.onStatusChange('closed');\n cleanupEventSource();\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 function scheduleReconnect(): void {\n if (disposed || reconnectTimer) return;\n callbacks.onStatusChange('reconnecting');\n\n cleanupEventSource();\n\n reconnectTimer = setTimeout(() => {\n reconnectTimer = null;\n connect();\n }, reconnectDelay);\n }\n\n function cleanupEventSource(): void {\n if (es) {\n es.removeAllEventListeners();\n es.close();\n es = null;\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 disconnect: () => {\n disposed = true;\n if (reconnectTimer) {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n cleanupEventSource();\n callbacks.onStatusChange('disconnected');\n },\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"],"mappings":"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","names":["EventSource"]}
|
|
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
|
@@ -18,9 +18,6 @@ var ChannelClosedError = class extends Error {
|
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
// src/session.ts
|
|
22
|
-
import EventSource from "react-native-sse";
|
|
23
|
-
|
|
24
21
|
// src/resolve-url.ts
|
|
25
22
|
function createManifest(serverUrl) {
|
|
26
23
|
return { buckets: [{ group: "default", range: [0, 99], server_url: serverUrl }] };
|
|
@@ -34,86 +31,68 @@ function resolveServerUrl(manifest, channelId) {
|
|
|
34
31
|
return bucket.server_url;
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
// src/
|
|
34
|
+
// src/sse-connection.ts
|
|
35
|
+
import EventSource from "react-native-sse";
|
|
38
36
|
var DEFAULT_RECONNECT_DELAY_MS = 3e3;
|
|
39
|
-
|
|
40
|
-
async function createChatSession(config, channelId, profile, callbacks) {
|
|
41
|
-
const serviceUrl = resolveServerUrl(config.manifest, channelId);
|
|
42
|
-
const customHeaders = config.headers ?? {};
|
|
37
|
+
function createSSEConnection(config, callbacks) {
|
|
43
38
|
const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
method: "POST",
|
|
47
|
-
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
48
|
-
body: JSON.stringify(profile)
|
|
49
|
-
});
|
|
50
|
-
if (joinRes.status === 410) {
|
|
51
|
-
throw new ChannelClosedError(channelId);
|
|
52
|
-
}
|
|
53
|
-
if (!joinRes.ok) {
|
|
54
|
-
throw new Error(`Join failed: ${joinRes.status} ${await joinRes.text()}`);
|
|
55
|
-
}
|
|
56
|
-
const { messages, participants, joined_at } = await joinRes.json();
|
|
57
|
-
let lastEventId = messages.length > 0 ? messages[messages.length - 1].id : void 0;
|
|
58
|
-
const joinedAt = joined_at;
|
|
59
|
-
const seenMessageIds = new Set(messages.map((m) => m.id));
|
|
39
|
+
const customEvents = config.customEvents ?? [];
|
|
40
|
+
let currentLastEventId = config.lastEventId;
|
|
60
41
|
let es = null;
|
|
61
42
|
let disposed = false;
|
|
62
43
|
let reconnectTimer = null;
|
|
63
|
-
function
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
seenMessageIds.add(id);
|
|
44
|
+
function cleanup() {
|
|
45
|
+
if (es) {
|
|
46
|
+
es.removeAllEventListeners();
|
|
47
|
+
es.close();
|
|
48
|
+
es = null;
|
|
69
49
|
}
|
|
70
50
|
}
|
|
51
|
+
function scheduleReconnect() {
|
|
52
|
+
if (disposed || reconnectTimer) return;
|
|
53
|
+
callbacks.onReconnecting?.();
|
|
54
|
+
cleanup();
|
|
55
|
+
reconnectTimer = setTimeout(() => {
|
|
56
|
+
reconnectTimer = null;
|
|
57
|
+
connect();
|
|
58
|
+
}, reconnectDelay);
|
|
59
|
+
}
|
|
71
60
|
function connect() {
|
|
72
61
|
if (disposed) return;
|
|
73
|
-
|
|
74
|
-
es = new EventSource(streamUrl, {
|
|
62
|
+
es = new EventSource(config.url, {
|
|
75
63
|
headers: {
|
|
76
|
-
...
|
|
77
|
-
...
|
|
64
|
+
...config.headers,
|
|
65
|
+
...currentLastEventId && { "Last-Event-ID": currentLastEventId }
|
|
78
66
|
},
|
|
79
67
|
pollingInterval: 0
|
|
80
68
|
});
|
|
81
69
|
es.addEventListener("open", () => {
|
|
82
70
|
if (disposed) return;
|
|
83
|
-
callbacks.
|
|
71
|
+
callbacks.onOpen?.();
|
|
84
72
|
});
|
|
85
73
|
es.addEventListener("message", (event) => {
|
|
86
74
|
if (disposed || !event.data) return;
|
|
87
|
-
let message;
|
|
88
|
-
try {
|
|
89
|
-
message = JSON.parse(event.data);
|
|
90
|
-
} catch {
|
|
91
|
-
callbacks.onError(new Error("Failed to parse SSE message"));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
75
|
if (event.lastEventId) {
|
|
95
|
-
|
|
76
|
+
currentLastEventId = event.lastEventId;
|
|
96
77
|
}
|
|
97
|
-
|
|
98
|
-
seenMessageIds.add(message.id);
|
|
99
|
-
trimSeenIds();
|
|
100
|
-
callbacks.onMessage(message);
|
|
101
|
-
});
|
|
102
|
-
es.addEventListener("resync", () => {
|
|
103
|
-
if (disposed) return;
|
|
104
|
-
cleanupEventSource();
|
|
105
|
-
callbacks.onResync();
|
|
78
|
+
callbacks.onEvent("message", event.data, event.lastEventId ?? void 0);
|
|
106
79
|
});
|
|
80
|
+
for (const eventName of customEvents) {
|
|
81
|
+
es.addEventListener(eventName, (event) => {
|
|
82
|
+
if (disposed) return;
|
|
83
|
+
callbacks.onEvent(eventName, event.data ?? "", void 0);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
107
86
|
es.addEventListener("error", (event) => {
|
|
108
87
|
if (disposed) return;
|
|
109
88
|
const msg = "message" in event ? String(event.message) : "";
|
|
110
89
|
if (msg.includes("Channel is closed") || msg.includes("410")) {
|
|
111
|
-
callbacks.
|
|
112
|
-
|
|
90
|
+
callbacks.onClosed?.();
|
|
91
|
+
cleanup();
|
|
113
92
|
disposed = true;
|
|
114
93
|
return;
|
|
115
94
|
}
|
|
116
|
-
if (msg) callbacks.onError(new Error(msg));
|
|
95
|
+
if (msg) callbacks.onError?.(new Error(msg));
|
|
117
96
|
scheduleReconnect();
|
|
118
97
|
});
|
|
119
98
|
es.addEventListener("close", () => {
|
|
@@ -121,22 +100,104 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
121
100
|
scheduleReconnect();
|
|
122
101
|
});
|
|
123
102
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
103
|
+
connect();
|
|
104
|
+
return {
|
|
105
|
+
close: () => {
|
|
106
|
+
disposed = true;
|
|
107
|
+
if (reconnectTimer) {
|
|
108
|
+
clearTimeout(reconnectTimer);
|
|
109
|
+
reconnectTimer = null;
|
|
110
|
+
}
|
|
111
|
+
cleanup();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/session.ts
|
|
117
|
+
var DEFAULT_RECONNECT_DELAY_MS2 = 3e3;
|
|
118
|
+
var MAX_SEEN_IDS = 500;
|
|
119
|
+
async function createChatSession(config, channelId, profile, callbacks) {
|
|
120
|
+
const serviceUrl = resolveServerUrl(config.manifest, channelId);
|
|
121
|
+
const customHeaders = config.headers ?? {};
|
|
122
|
+
const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS2;
|
|
123
|
+
callbacks.onStatusChange("connecting");
|
|
124
|
+
const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
127
|
+
body: JSON.stringify(profile)
|
|
128
|
+
});
|
|
129
|
+
if (joinRes.status === 410) {
|
|
130
|
+
throw new ChannelClosedError(channelId);
|
|
132
131
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
if (!joinRes.ok) {
|
|
133
|
+
throw new Error(`Join failed: ${joinRes.status} ${await joinRes.text()}`);
|
|
134
|
+
}
|
|
135
|
+
const { messages, participants, joined_at } = await joinRes.json();
|
|
136
|
+
let lastEventId = messages.length > 0 ? messages[messages.length - 1].id : void 0;
|
|
137
|
+
const joinedAt = joined_at;
|
|
138
|
+
const seenMessageIds = new Set(messages.map((m) => m.id));
|
|
139
|
+
let sseConn = null;
|
|
140
|
+
let disposed = false;
|
|
141
|
+
const TRIM_THRESHOLD = MAX_SEEN_IDS * 1.5;
|
|
142
|
+
function trimSeenIds() {
|
|
143
|
+
if (seenMessageIds.size <= TRIM_THRESHOLD) return;
|
|
144
|
+
const ids = [...seenMessageIds];
|
|
145
|
+
seenMessageIds.clear();
|
|
146
|
+
for (const id of ids.slice(-MAX_SEEN_IDS)) {
|
|
147
|
+
seenMessageIds.add(id);
|
|
138
148
|
}
|
|
139
149
|
}
|
|
150
|
+
function connect() {
|
|
151
|
+
if (disposed) return;
|
|
152
|
+
const streamUrl = lastEventId ? `${serviceUrl}/channels/${channelId}/stream` : `${serviceUrl}/channels/${channelId}/stream?since_time=${encodeURIComponent(joinedAt)}`;
|
|
153
|
+
sseConn = createSSEConnection(
|
|
154
|
+
{
|
|
155
|
+
url: streamUrl,
|
|
156
|
+
headers: customHeaders,
|
|
157
|
+
reconnectDelayMs: reconnectDelay,
|
|
158
|
+
lastEventId,
|
|
159
|
+
customEvents: ["resync"]
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
onOpen: () => {
|
|
163
|
+
if (!disposed) callbacks.onStatusChange("connected");
|
|
164
|
+
},
|
|
165
|
+
onEvent: (eventType, data, eventId) => {
|
|
166
|
+
if (disposed) return;
|
|
167
|
+
if (eventType === "message") {
|
|
168
|
+
let message;
|
|
169
|
+
try {
|
|
170
|
+
message = JSON.parse(data);
|
|
171
|
+
} catch {
|
|
172
|
+
callbacks.onError(new Error("Failed to parse SSE message"));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (eventId) {
|
|
176
|
+
lastEventId = eventId;
|
|
177
|
+
}
|
|
178
|
+
if (seenMessageIds.has(message.id)) return;
|
|
179
|
+
seenMessageIds.add(message.id);
|
|
180
|
+
trimSeenIds();
|
|
181
|
+
callbacks.onMessage(message);
|
|
182
|
+
} else if (eventType === "resync") {
|
|
183
|
+
sseConn?.close();
|
|
184
|
+
sseConn = null;
|
|
185
|
+
callbacks.onResync();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
onError: (err) => {
|
|
189
|
+
if (!disposed) callbacks.onError(err);
|
|
190
|
+
},
|
|
191
|
+
onClosed: () => {
|
|
192
|
+
callbacks.onStatusChange("closed");
|
|
193
|
+
disposed = true;
|
|
194
|
+
},
|
|
195
|
+
onReconnecting: () => {
|
|
196
|
+
if (!disposed) callbacks.onStatusChange("reconnecting");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
}
|
|
140
201
|
connect();
|
|
141
202
|
return {
|
|
142
203
|
serviceUrl,
|
|
@@ -166,13 +227,23 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
166
227
|
seenMessageIds.add(response.id);
|
|
167
228
|
return response;
|
|
168
229
|
},
|
|
230
|
+
markAsRead: async (messageId) => {
|
|
231
|
+
const res = await fetch(`${serviceUrl}/channels/${channelId}/read`, {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
participant_id: profile.id,
|
|
236
|
+
message_id: messageId
|
|
237
|
+
})
|
|
238
|
+
});
|
|
239
|
+
if (!res.ok) {
|
|
240
|
+
throw new Error(`markAsRead failed: ${res.status}`);
|
|
241
|
+
}
|
|
242
|
+
},
|
|
169
243
|
disconnect: () => {
|
|
170
244
|
disposed = true;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
reconnectTimer = null;
|
|
174
|
-
}
|
|
175
|
-
cleanupEventSource();
|
|
245
|
+
sseConn?.close();
|
|
246
|
+
sseConn = null;
|
|
176
247
|
callbacks.onStatusChange("disconnected");
|
|
177
248
|
}
|
|
178
249
|
};
|
|
@@ -187,6 +258,8 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
187
258
|
const [error, setError] = useState(null);
|
|
188
259
|
const sessionRef = useRef(null);
|
|
189
260
|
const disposedRef = useRef(false);
|
|
261
|
+
const messagesRef = useRef(messages);
|
|
262
|
+
messagesRef.current = messages;
|
|
190
263
|
const statusRef = useRef(status);
|
|
191
264
|
statusRef.current = status;
|
|
192
265
|
const appStateRef = useRef(AppState.currentState);
|
|
@@ -204,6 +277,9 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
204
277
|
onMessage: (message) => {
|
|
205
278
|
if (disposedRef.current) return;
|
|
206
279
|
setMessages((prev) => {
|
|
280
|
+
if (pendingOptimisticIds.current.size === 0) {
|
|
281
|
+
return [...prev, message];
|
|
282
|
+
}
|
|
207
283
|
const optimisticIdx = prev.findIndex(
|
|
208
284
|
(m) => pendingOptimisticIds.current.has(m.id) && m.sender_id === message.sender_id && m.body === message.body && m.type === message.type
|
|
209
285
|
);
|
|
@@ -267,6 +343,13 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
267
343
|
startSession();
|
|
268
344
|
return () => {
|
|
269
345
|
disposedRef.current = true;
|
|
346
|
+
if (statusRef.current === "connected" && sessionRef.current) {
|
|
347
|
+
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
|
|
348
|
+
if (lastMsg) {
|
|
349
|
+
sessionRef.current.markAsRead(lastMsg.id).catch(() => {
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
270
353
|
if (backgroundTimerRef.current) {
|
|
271
354
|
clearTimeout(backgroundTimerRef.current);
|
|
272
355
|
backgroundTimerRef.current = null;
|
|
@@ -366,12 +449,134 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
366
449
|
}, []);
|
|
367
450
|
return { messages, participants, status, error, sendMessage, disconnect };
|
|
368
451
|
}
|
|
452
|
+
|
|
453
|
+
// src/use-unread.ts
|
|
454
|
+
import { useEffect as useEffect2, useRef as useRef2, useState as useState2, useCallback as useCallback2 } from "react";
|
|
455
|
+
import { AppState as AppState2, Platform as Platform2 } from "react-native";
|
|
456
|
+
var DEFAULT_BACKGROUND_GRACE_MS2 = 2e3;
|
|
457
|
+
var UNREAD_CUSTOM_EVENTS = ["unread_snapshot", "unread_update", "unread_clear"];
|
|
458
|
+
function useUnread(options) {
|
|
459
|
+
const { config, channelId, participantId, enabled = true } = options;
|
|
460
|
+
const [unreadCount, setUnreadCount] = useState2(0);
|
|
461
|
+
const [lastMessageAt, setLastMessageAt] = useState2(null);
|
|
462
|
+
const [error, setError] = useState2(null);
|
|
463
|
+
const connRef = useRef2(null);
|
|
464
|
+
const configRef = useRef2(config);
|
|
465
|
+
configRef.current = config;
|
|
466
|
+
const appStateRef = useRef2(AppState2.currentState);
|
|
467
|
+
const backgroundTimerRef = useRef2(null);
|
|
468
|
+
const backgroundGraceMs = config.backgroundGraceMs ?? (Platform2.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS2 : 0);
|
|
469
|
+
const connect = useCallback2(() => {
|
|
470
|
+
connRef.current?.close();
|
|
471
|
+
connRef.current = null;
|
|
472
|
+
const serviceUrl = resolveServerUrl(configRef.current.manifest, channelId);
|
|
473
|
+
const customHeaders = configRef.current.headers ?? {};
|
|
474
|
+
const url = `${serviceUrl}/channels/${channelId}/unread?participant_id=${encodeURIComponent(participantId)}`;
|
|
475
|
+
connRef.current = createSSEConnection(
|
|
476
|
+
{
|
|
477
|
+
url,
|
|
478
|
+
headers: customHeaders,
|
|
479
|
+
reconnectDelayMs: configRef.current.reconnectDelayMs,
|
|
480
|
+
customEvents: UNREAD_CUSTOM_EVENTS
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
onOpen: () => {
|
|
484
|
+
setError(null);
|
|
485
|
+
},
|
|
486
|
+
onEvent: (eventType, data) => {
|
|
487
|
+
try {
|
|
488
|
+
if (eventType === "unread_snapshot") {
|
|
489
|
+
const snapshot = JSON.parse(data);
|
|
490
|
+
setUnreadCount(snapshot.unread_count);
|
|
491
|
+
setLastMessageAt(snapshot.last_message_at);
|
|
492
|
+
} else if (eventType === "unread_update") {
|
|
493
|
+
const update = JSON.parse(data);
|
|
494
|
+
setUnreadCount((prev) => prev + 1);
|
|
495
|
+
setLastMessageAt(update.created_at);
|
|
496
|
+
} else if (eventType === "unread_clear") {
|
|
497
|
+
const clear = JSON.parse(data);
|
|
498
|
+
setUnreadCount(clear.unread_count);
|
|
499
|
+
}
|
|
500
|
+
} catch {
|
|
501
|
+
setError(new Error("Failed to parse unread SSE event"));
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
onError: (err) => {
|
|
505
|
+
setError(err);
|
|
506
|
+
},
|
|
507
|
+
onClosed: () => {
|
|
508
|
+
connRef.current = null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
}, [channelId, participantId]);
|
|
513
|
+
const disconnect = useCallback2(() => {
|
|
514
|
+
connRef.current?.close();
|
|
515
|
+
connRef.current = null;
|
|
516
|
+
}, []);
|
|
517
|
+
useEffect2(() => {
|
|
518
|
+
setUnreadCount(0);
|
|
519
|
+
setLastMessageAt(null);
|
|
520
|
+
setError(null);
|
|
521
|
+
if (!enabled) {
|
|
522
|
+
disconnect();
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
connect();
|
|
526
|
+
return () => {
|
|
527
|
+
disconnect();
|
|
528
|
+
if (backgroundTimerRef.current) {
|
|
529
|
+
clearTimeout(backgroundTimerRef.current);
|
|
530
|
+
backgroundTimerRef.current = null;
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}, [channelId, participantId, enabled, connect, disconnect]);
|
|
534
|
+
useEffect2(() => {
|
|
535
|
+
if (!enabled) return;
|
|
536
|
+
const subscription = AppState2.addEventListener("change", (nextAppState) => {
|
|
537
|
+
const prev = appStateRef.current;
|
|
538
|
+
appStateRef.current = nextAppState;
|
|
539
|
+
if (!connRef.current && nextAppState !== "active") return;
|
|
540
|
+
const shouldTeardown = nextAppState === "background" || Platform2.OS === "ios" && nextAppState === "inactive";
|
|
541
|
+
if (nextAppState === "active") {
|
|
542
|
+
if (backgroundTimerRef.current) {
|
|
543
|
+
clearTimeout(backgroundTimerRef.current);
|
|
544
|
+
backgroundTimerRef.current = null;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (prev.match(/inactive|background/) && !connRef.current) {
|
|
548
|
+
connect();
|
|
549
|
+
}
|
|
550
|
+
} else if (shouldTeardown) {
|
|
551
|
+
if (backgroundTimerRef.current) return;
|
|
552
|
+
if (backgroundGraceMs === 0) {
|
|
553
|
+
disconnect();
|
|
554
|
+
} else {
|
|
555
|
+
backgroundTimerRef.current = setTimeout(() => {
|
|
556
|
+
backgroundTimerRef.current = null;
|
|
557
|
+
disconnect();
|
|
558
|
+
}, backgroundGraceMs);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
return () => {
|
|
563
|
+
subscription.remove();
|
|
564
|
+
if (backgroundTimerRef.current) {
|
|
565
|
+
clearTimeout(backgroundTimerRef.current);
|
|
566
|
+
backgroundTimerRef.current = null;
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
}, [enabled, backgroundGraceMs, connect, disconnect]);
|
|
570
|
+
return { unreadCount, hasUnread: unreadCount > 0, lastMessageAt, error };
|
|
571
|
+
}
|
|
369
572
|
export {
|
|
370
573
|
ChannelClosedError,
|
|
371
574
|
ChatDisconnectedError,
|
|
372
575
|
createChatSession,
|
|
373
576
|
createManifest,
|
|
577
|
+
createSSEConnection,
|
|
374
578
|
resolveServerUrl,
|
|
375
|
-
useChat
|
|
579
|
+
useChat,
|
|
580
|
+
useUnread
|
|
376
581
|
};
|
|
377
582
|
//# sourceMappingURL=index.mjs.map
|