@pedi/chika-sdk 1.0.0 → 1.0.4
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/dist/index.d.mts +36 -8
- package/dist/index.d.ts +36 -8
- package/dist/index.js +301 -80
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +298 -79
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChatManifest, ChatDomain, DefaultDomain, Participant, Message, MessageAttributes, SendMessageResponse } from '@pedi/chika-types';
|
|
2
|
-
export { ChatBucket, ChatDomain, ChatManifest, DefaultDomain, Message, MessageAttributes, Participant, PediChat, PediLocation, PediMessageAttributes, PediMessageType, PediParticipantMeta, PediRole, PediVehicle, SendMessageResponse } from '@pedi/chika-types';
|
|
2
|
+
export { ChatBucket, ChatDomain, ChatManifest, DefaultDomain, MarkReadRequest, Message, MessageAttributes, Participant, PediChat, PediLocation, PediMessageAttributes, PediMessageType, PediParticipantMeta, PediRole, PediVehicle, SSEUnreadClearEvent, SSEUnreadEvent, SSEUnreadUpdateEvent, SendMessageResponse, UnreadCountResponse } from '@pedi/chika-types';
|
|
3
3
|
|
|
4
4
|
type ChatStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'closed' | 'error';
|
|
5
5
|
/**
|
|
@@ -49,6 +49,20 @@ interface UseChatReturn<D extends ChatDomain = DefaultDomain> {
|
|
|
49
49
|
*/
|
|
50
50
|
declare function useChat<D extends ChatDomain = DefaultDomain>({ config, channelId, profile, onMessage }: UseChatOptions<D>): UseChatReturn<D>;
|
|
51
51
|
|
|
52
|
+
interface UseUnreadOptions {
|
|
53
|
+
config: ChatConfig;
|
|
54
|
+
channelId: string;
|
|
55
|
+
participantId: string;
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
}
|
|
58
|
+
interface UseUnreadReturn {
|
|
59
|
+
unreadCount: number;
|
|
60
|
+
hasUnread: boolean;
|
|
61
|
+
lastMessageAt: string | null;
|
|
62
|
+
error: Error | null;
|
|
63
|
+
}
|
|
64
|
+
declare function useUnread(options: UseUnreadOptions): UseUnreadReturn;
|
|
65
|
+
|
|
52
66
|
interface SessionCallbacks<D extends ChatDomain = DefaultDomain> {
|
|
53
67
|
onMessage: (message: Message<D>) => void;
|
|
54
68
|
onStatusChange: (status: ChatStatus) => void;
|
|
@@ -61,14 +75,9 @@ interface ChatSession<D extends ChatDomain = DefaultDomain> {
|
|
|
61
75
|
initialParticipants: Participant<D>[];
|
|
62
76
|
initialMessages: Message<D>[];
|
|
63
77
|
sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;
|
|
78
|
+
markAsRead: (messageId: string) => Promise<void>;
|
|
64
79
|
disconnect: () => void;
|
|
65
80
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Creates an imperative chat session with SSE streaming and managed reconnection.
|
|
68
|
-
* Lower-level API — prefer `useChat` hook for React Native components.
|
|
69
|
-
*
|
|
70
|
-
* @template D - Chat domain type. Defaults to DefaultDomain.
|
|
71
|
-
*/
|
|
72
81
|
declare function createChatSession<D extends ChatDomain = DefaultDomain>(config: ChatConfig, channelId: string, profile: Participant<D>, callbacks: SessionCallbacks<D>): Promise<ChatSession<D>>;
|
|
73
82
|
|
|
74
83
|
/**
|
|
@@ -82,6 +91,25 @@ declare function createChatSession<D extends ChatDomain = DefaultDomain>(config:
|
|
|
82
91
|
declare function createManifest(serverUrl: string): ChatManifest;
|
|
83
92
|
declare function resolveServerUrl(manifest: ChatManifest, channelId: string): string;
|
|
84
93
|
|
|
94
|
+
interface SSEConnectionConfig {
|
|
95
|
+
url: string;
|
|
96
|
+
headers?: Record<string, string>;
|
|
97
|
+
reconnectDelayMs?: number;
|
|
98
|
+
lastEventId?: string;
|
|
99
|
+
customEvents?: string[];
|
|
100
|
+
}
|
|
101
|
+
interface SSEConnectionCallbacks {
|
|
102
|
+
onOpen?: () => void;
|
|
103
|
+
onEvent: (eventType: string, data: string, lastEventId?: string) => void;
|
|
104
|
+
onError?: (error: Error) => void;
|
|
105
|
+
onClosed?: () => void;
|
|
106
|
+
onReconnecting?: () => void;
|
|
107
|
+
}
|
|
108
|
+
interface SSEConnection {
|
|
109
|
+
close: () => void;
|
|
110
|
+
}
|
|
111
|
+
declare function createSSEConnection(config: SSEConnectionConfig, callbacks: SSEConnectionCallbacks): SSEConnection;
|
|
112
|
+
|
|
85
113
|
declare class ChatDisconnectedError extends Error {
|
|
86
114
|
readonly status: ChatStatus;
|
|
87
115
|
constructor(status: ChatStatus);
|
|
@@ -91,4 +119,4 @@ declare class ChannelClosedError extends Error {
|
|
|
91
119
|
constructor(channelId: string);
|
|
92
120
|
}
|
|
93
121
|
|
|
94
|
-
export { ChannelClosedError, type ChatConfig, ChatDisconnectedError, type ChatSession, type ChatStatus, type SessionCallbacks, type UseChatOptions, type UseChatReturn, createChatSession, createManifest, resolveServerUrl, useChat };
|
|
122
|
+
export { ChannelClosedError, type ChatConfig, ChatDisconnectedError, type ChatSession, type ChatStatus, type SSEConnection, type SSEConnectionCallbacks, type SSEConnectionConfig, type SessionCallbacks, type UseChatOptions, type UseChatReturn, type UseUnreadOptions, type UseUnreadReturn, createChatSession, createManifest, createSSEConnection, resolveServerUrl, useChat, useUnread };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChatManifest, ChatDomain, DefaultDomain, Participant, Message, MessageAttributes, SendMessageResponse } from '@pedi/chika-types';
|
|
2
|
-
export { ChatBucket, ChatDomain, ChatManifest, DefaultDomain, Message, MessageAttributes, Participant, PediChat, PediLocation, PediMessageAttributes, PediMessageType, PediParticipantMeta, PediRole, PediVehicle, SendMessageResponse } from '@pedi/chika-types';
|
|
2
|
+
export { ChatBucket, ChatDomain, ChatManifest, DefaultDomain, MarkReadRequest, Message, MessageAttributes, Participant, PediChat, PediLocation, PediMessageAttributes, PediMessageType, PediParticipantMeta, PediRole, PediVehicle, SSEUnreadClearEvent, SSEUnreadEvent, SSEUnreadUpdateEvent, SendMessageResponse, UnreadCountResponse } from '@pedi/chika-types';
|
|
3
3
|
|
|
4
4
|
type ChatStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'closed' | 'error';
|
|
5
5
|
/**
|
|
@@ -49,6 +49,20 @@ interface UseChatReturn<D extends ChatDomain = DefaultDomain> {
|
|
|
49
49
|
*/
|
|
50
50
|
declare function useChat<D extends ChatDomain = DefaultDomain>({ config, channelId, profile, onMessage }: UseChatOptions<D>): UseChatReturn<D>;
|
|
51
51
|
|
|
52
|
+
interface UseUnreadOptions {
|
|
53
|
+
config: ChatConfig;
|
|
54
|
+
channelId: string;
|
|
55
|
+
participantId: string;
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
}
|
|
58
|
+
interface UseUnreadReturn {
|
|
59
|
+
unreadCount: number;
|
|
60
|
+
hasUnread: boolean;
|
|
61
|
+
lastMessageAt: string | null;
|
|
62
|
+
error: Error | null;
|
|
63
|
+
}
|
|
64
|
+
declare function useUnread(options: UseUnreadOptions): UseUnreadReturn;
|
|
65
|
+
|
|
52
66
|
interface SessionCallbacks<D extends ChatDomain = DefaultDomain> {
|
|
53
67
|
onMessage: (message: Message<D>) => void;
|
|
54
68
|
onStatusChange: (status: ChatStatus) => void;
|
|
@@ -61,14 +75,9 @@ interface ChatSession<D extends ChatDomain = DefaultDomain> {
|
|
|
61
75
|
initialParticipants: Participant<D>[];
|
|
62
76
|
initialMessages: Message<D>[];
|
|
63
77
|
sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;
|
|
78
|
+
markAsRead: (messageId: string) => Promise<void>;
|
|
64
79
|
disconnect: () => void;
|
|
65
80
|
}
|
|
66
|
-
/**
|
|
67
|
-
* Creates an imperative chat session with SSE streaming and managed reconnection.
|
|
68
|
-
* Lower-level API — prefer `useChat` hook for React Native components.
|
|
69
|
-
*
|
|
70
|
-
* @template D - Chat domain type. Defaults to DefaultDomain.
|
|
71
|
-
*/
|
|
72
81
|
declare function createChatSession<D extends ChatDomain = DefaultDomain>(config: ChatConfig, channelId: string, profile: Participant<D>, callbacks: SessionCallbacks<D>): Promise<ChatSession<D>>;
|
|
73
82
|
|
|
74
83
|
/**
|
|
@@ -82,6 +91,25 @@ declare function createChatSession<D extends ChatDomain = DefaultDomain>(config:
|
|
|
82
91
|
declare function createManifest(serverUrl: string): ChatManifest;
|
|
83
92
|
declare function resolveServerUrl(manifest: ChatManifest, channelId: string): string;
|
|
84
93
|
|
|
94
|
+
interface SSEConnectionConfig {
|
|
95
|
+
url: string;
|
|
96
|
+
headers?: Record<string, string>;
|
|
97
|
+
reconnectDelayMs?: number;
|
|
98
|
+
lastEventId?: string;
|
|
99
|
+
customEvents?: string[];
|
|
100
|
+
}
|
|
101
|
+
interface SSEConnectionCallbacks {
|
|
102
|
+
onOpen?: () => void;
|
|
103
|
+
onEvent: (eventType: string, data: string, lastEventId?: string) => void;
|
|
104
|
+
onError?: (error: Error) => void;
|
|
105
|
+
onClosed?: () => void;
|
|
106
|
+
onReconnecting?: () => void;
|
|
107
|
+
}
|
|
108
|
+
interface SSEConnection {
|
|
109
|
+
close: () => void;
|
|
110
|
+
}
|
|
111
|
+
declare function createSSEConnection(config: SSEConnectionConfig, callbacks: SSEConnectionCallbacks): SSEConnection;
|
|
112
|
+
|
|
85
113
|
declare class ChatDisconnectedError extends Error {
|
|
86
114
|
readonly status: ChatStatus;
|
|
87
115
|
constructor(status: ChatStatus);
|
|
@@ -91,4 +119,4 @@ declare class ChannelClosedError extends Error {
|
|
|
91
119
|
constructor(channelId: string);
|
|
92
120
|
}
|
|
93
121
|
|
|
94
|
-
export { ChannelClosedError, type ChatConfig, ChatDisconnectedError, type ChatSession, type ChatStatus, type SessionCallbacks, type UseChatOptions, type UseChatReturn, createChatSession, createManifest, resolveServerUrl, useChat };
|
|
122
|
+
export { ChannelClosedError, type ChatConfig, ChatDisconnectedError, type ChatSession, type ChatStatus, type SSEConnection, type SSEConnectionCallbacks, type SSEConnectionConfig, type SessionCallbacks, type UseChatOptions, type UseChatReturn, type UseUnreadOptions, type UseUnreadReturn, createChatSession, createManifest, createSSEConnection, resolveServerUrl, useChat, useUnread };
|
package/dist/index.js
CHANGED
|
@@ -34,8 +34,10 @@ __export(index_exports, {
|
|
|
34
34
|
ChatDisconnectedError: () => ChatDisconnectedError,
|
|
35
35
|
createChatSession: () => createChatSession,
|
|
36
36
|
createManifest: () => createManifest,
|
|
37
|
+
createSSEConnection: () => createSSEConnection,
|
|
37
38
|
resolveServerUrl: () => resolveServerUrl,
|
|
38
|
-
useChat: () => useChat
|
|
39
|
+
useChat: () => useChat,
|
|
40
|
+
useUnread: () => useUnread
|
|
39
41
|
});
|
|
40
42
|
module.exports = __toCommonJS(index_exports);
|
|
41
43
|
|
|
@@ -59,9 +61,6 @@ var ChannelClosedError = class extends Error {
|
|
|
59
61
|
}
|
|
60
62
|
};
|
|
61
63
|
|
|
62
|
-
// src/session.ts
|
|
63
|
-
var import_react_native_sse = __toESM(require("react-native-sse"));
|
|
64
|
-
|
|
65
64
|
// src/resolve-url.ts
|
|
66
65
|
function createManifest(serverUrl) {
|
|
67
66
|
return { buckets: [{ group: "default", range: [0, 99], server_url: serverUrl }] };
|
|
@@ -75,86 +74,68 @@ function resolveServerUrl(manifest, channelId) {
|
|
|
75
74
|
return bucket.server_url;
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
// src/
|
|
77
|
+
// src/sse-connection.ts
|
|
78
|
+
var import_react_native_sse = __toESM(require("react-native-sse"));
|
|
79
79
|
var DEFAULT_RECONNECT_DELAY_MS = 3e3;
|
|
80
|
-
|
|
81
|
-
async function createChatSession(config, channelId, profile, callbacks) {
|
|
82
|
-
const serviceUrl = resolveServerUrl(config.manifest, channelId);
|
|
83
|
-
const customHeaders = config.headers ?? {};
|
|
80
|
+
function createSSEConnection(config, callbacks) {
|
|
84
81
|
const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
method: "POST",
|
|
88
|
-
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
89
|
-
body: JSON.stringify(profile)
|
|
90
|
-
});
|
|
91
|
-
if (joinRes.status === 410) {
|
|
92
|
-
throw new ChannelClosedError(channelId);
|
|
93
|
-
}
|
|
94
|
-
if (!joinRes.ok) {
|
|
95
|
-
throw new Error(`Join failed: ${joinRes.status} ${await joinRes.text()}`);
|
|
96
|
-
}
|
|
97
|
-
const { messages, participants, joined_at } = await joinRes.json();
|
|
98
|
-
let lastEventId = messages.length > 0 ? messages[messages.length - 1].id : void 0;
|
|
99
|
-
const joinedAt = joined_at;
|
|
100
|
-
const seenMessageIds = new Set(messages.map((m) => m.id));
|
|
82
|
+
const customEvents = config.customEvents ?? [];
|
|
83
|
+
let currentLastEventId = config.lastEventId;
|
|
101
84
|
let es = null;
|
|
102
85
|
let disposed = false;
|
|
103
86
|
let reconnectTimer = null;
|
|
104
|
-
function
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
seenMessageIds.add(id);
|
|
87
|
+
function cleanup() {
|
|
88
|
+
if (es) {
|
|
89
|
+
es.removeAllEventListeners();
|
|
90
|
+
es.close();
|
|
91
|
+
es = null;
|
|
110
92
|
}
|
|
111
93
|
}
|
|
94
|
+
function scheduleReconnect() {
|
|
95
|
+
if (disposed || reconnectTimer) return;
|
|
96
|
+
callbacks.onReconnecting?.();
|
|
97
|
+
cleanup();
|
|
98
|
+
reconnectTimer = setTimeout(() => {
|
|
99
|
+
reconnectTimer = null;
|
|
100
|
+
connect();
|
|
101
|
+
}, reconnectDelay);
|
|
102
|
+
}
|
|
112
103
|
function connect() {
|
|
113
104
|
if (disposed) return;
|
|
114
|
-
|
|
115
|
-
es = new import_react_native_sse.default(streamUrl, {
|
|
105
|
+
es = new import_react_native_sse.default(config.url, {
|
|
116
106
|
headers: {
|
|
117
|
-
...
|
|
118
|
-
...
|
|
107
|
+
...config.headers,
|
|
108
|
+
...currentLastEventId && { "Last-Event-ID": currentLastEventId }
|
|
119
109
|
},
|
|
120
110
|
pollingInterval: 0
|
|
121
111
|
});
|
|
122
112
|
es.addEventListener("open", () => {
|
|
123
113
|
if (disposed) return;
|
|
124
|
-
callbacks.
|
|
114
|
+
callbacks.onOpen?.();
|
|
125
115
|
});
|
|
126
116
|
es.addEventListener("message", (event) => {
|
|
127
117
|
if (disposed || !event.data) return;
|
|
128
|
-
let message;
|
|
129
|
-
try {
|
|
130
|
-
message = JSON.parse(event.data);
|
|
131
|
-
} catch {
|
|
132
|
-
callbacks.onError(new Error("Failed to parse SSE message"));
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
118
|
if (event.lastEventId) {
|
|
136
|
-
|
|
119
|
+
currentLastEventId = event.lastEventId;
|
|
137
120
|
}
|
|
138
|
-
|
|
139
|
-
seenMessageIds.add(message.id);
|
|
140
|
-
trimSeenIds();
|
|
141
|
-
callbacks.onMessage(message);
|
|
142
|
-
});
|
|
143
|
-
es.addEventListener("resync", () => {
|
|
144
|
-
if (disposed) return;
|
|
145
|
-
cleanupEventSource();
|
|
146
|
-
callbacks.onResync();
|
|
121
|
+
callbacks.onEvent("message", event.data, event.lastEventId ?? void 0);
|
|
147
122
|
});
|
|
123
|
+
for (const eventName of customEvents) {
|
|
124
|
+
es.addEventListener(eventName, (event) => {
|
|
125
|
+
if (disposed) return;
|
|
126
|
+
callbacks.onEvent(eventName, event.data ?? "", void 0);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
148
129
|
es.addEventListener("error", (event) => {
|
|
149
130
|
if (disposed) return;
|
|
150
131
|
const msg = "message" in event ? String(event.message) : "";
|
|
151
132
|
if (msg.includes("Channel is closed") || msg.includes("410")) {
|
|
152
|
-
callbacks.
|
|
153
|
-
|
|
133
|
+
callbacks.onClosed?.();
|
|
134
|
+
cleanup();
|
|
154
135
|
disposed = true;
|
|
155
136
|
return;
|
|
156
137
|
}
|
|
157
|
-
if (msg) callbacks.onError(new Error(msg));
|
|
138
|
+
if (msg) callbacks.onError?.(new Error(msg));
|
|
158
139
|
scheduleReconnect();
|
|
159
140
|
});
|
|
160
141
|
es.addEventListener("close", () => {
|
|
@@ -162,22 +143,103 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
162
143
|
scheduleReconnect();
|
|
163
144
|
});
|
|
164
145
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
146
|
+
connect();
|
|
147
|
+
return {
|
|
148
|
+
close: () => {
|
|
149
|
+
disposed = true;
|
|
150
|
+
if (reconnectTimer) {
|
|
151
|
+
clearTimeout(reconnectTimer);
|
|
152
|
+
reconnectTimer = null;
|
|
153
|
+
}
|
|
154
|
+
cleanup();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/session.ts
|
|
160
|
+
var DEFAULT_RECONNECT_DELAY_MS2 = 3e3;
|
|
161
|
+
var MAX_SEEN_IDS = 500;
|
|
162
|
+
async function createChatSession(config, channelId, profile, callbacks) {
|
|
163
|
+
const serviceUrl = resolveServerUrl(config.manifest, channelId);
|
|
164
|
+
const customHeaders = config.headers ?? {};
|
|
165
|
+
const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS2;
|
|
166
|
+
callbacks.onStatusChange("connecting");
|
|
167
|
+
const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {
|
|
168
|
+
method: "POST",
|
|
169
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
170
|
+
body: JSON.stringify(profile)
|
|
171
|
+
});
|
|
172
|
+
if (joinRes.status === 410) {
|
|
173
|
+
throw new ChannelClosedError(channelId);
|
|
173
174
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
if (!joinRes.ok) {
|
|
176
|
+
throw new Error(`Join failed: ${joinRes.status} ${await joinRes.text()}`);
|
|
177
|
+
}
|
|
178
|
+
const { messages, participants, joined_at } = await joinRes.json();
|
|
179
|
+
let lastEventId = messages.length > 0 ? messages[messages.length - 1].id : void 0;
|
|
180
|
+
const joinedAt = joined_at;
|
|
181
|
+
const seenMessageIds = new Set(messages.map((m) => m.id));
|
|
182
|
+
let sseConn = null;
|
|
183
|
+
let disposed = false;
|
|
184
|
+
function trimSeenIds() {
|
|
185
|
+
if (seenMessageIds.size <= MAX_SEEN_IDS) return;
|
|
186
|
+
const ids = [...seenMessageIds];
|
|
187
|
+
seenMessageIds.clear();
|
|
188
|
+
for (const id of ids.slice(-MAX_SEEN_IDS)) {
|
|
189
|
+
seenMessageIds.add(id);
|
|
179
190
|
}
|
|
180
191
|
}
|
|
192
|
+
function connect() {
|
|
193
|
+
if (disposed) return;
|
|
194
|
+
const streamUrl = lastEventId ? `${serviceUrl}/channels/${channelId}/stream` : `${serviceUrl}/channels/${channelId}/stream?since_time=${encodeURIComponent(joinedAt)}`;
|
|
195
|
+
sseConn = createSSEConnection(
|
|
196
|
+
{
|
|
197
|
+
url: streamUrl,
|
|
198
|
+
headers: customHeaders,
|
|
199
|
+
reconnectDelayMs: reconnectDelay,
|
|
200
|
+
lastEventId,
|
|
201
|
+
customEvents: ["resync"]
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
onOpen: () => {
|
|
205
|
+
if (!disposed) callbacks.onStatusChange("connected");
|
|
206
|
+
},
|
|
207
|
+
onEvent: (eventType, data, eventId) => {
|
|
208
|
+
if (disposed) return;
|
|
209
|
+
if (eventType === "message") {
|
|
210
|
+
let message;
|
|
211
|
+
try {
|
|
212
|
+
message = JSON.parse(data);
|
|
213
|
+
} catch {
|
|
214
|
+
callbacks.onError(new Error("Failed to parse SSE message"));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (eventId) {
|
|
218
|
+
lastEventId = eventId;
|
|
219
|
+
}
|
|
220
|
+
if (seenMessageIds.has(message.id)) return;
|
|
221
|
+
seenMessageIds.add(message.id);
|
|
222
|
+
trimSeenIds();
|
|
223
|
+
callbacks.onMessage(message);
|
|
224
|
+
} else if (eventType === "resync") {
|
|
225
|
+
sseConn?.close();
|
|
226
|
+
sseConn = null;
|
|
227
|
+
callbacks.onResync();
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
onError: (err) => {
|
|
231
|
+
if (!disposed) callbacks.onError(err);
|
|
232
|
+
},
|
|
233
|
+
onClosed: () => {
|
|
234
|
+
callbacks.onStatusChange("closed");
|
|
235
|
+
disposed = true;
|
|
236
|
+
},
|
|
237
|
+
onReconnecting: () => {
|
|
238
|
+
if (!disposed) callbacks.onStatusChange("reconnecting");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
}
|
|
181
243
|
connect();
|
|
182
244
|
return {
|
|
183
245
|
serviceUrl,
|
|
@@ -207,13 +269,23 @@ async function createChatSession(config, channelId, profile, callbacks) {
|
|
|
207
269
|
seenMessageIds.add(response.id);
|
|
208
270
|
return response;
|
|
209
271
|
},
|
|
272
|
+
markAsRead: async (messageId) => {
|
|
273
|
+
const res = await fetch(`${serviceUrl}/channels/${channelId}/read`, {
|
|
274
|
+
method: "POST",
|
|
275
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
276
|
+
body: JSON.stringify({
|
|
277
|
+
participant_id: profile.id,
|
|
278
|
+
message_id: messageId
|
|
279
|
+
})
|
|
280
|
+
});
|
|
281
|
+
if (!res.ok) {
|
|
282
|
+
throw new Error(`markAsRead failed: ${res.status}`);
|
|
283
|
+
}
|
|
284
|
+
},
|
|
210
285
|
disconnect: () => {
|
|
211
286
|
disposed = true;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
reconnectTimer = null;
|
|
215
|
-
}
|
|
216
|
-
cleanupEventSource();
|
|
287
|
+
sseConn?.close();
|
|
288
|
+
sseConn = null;
|
|
217
289
|
callbacks.onStatusChange("disconnected");
|
|
218
290
|
}
|
|
219
291
|
};
|
|
@@ -228,6 +300,8 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
228
300
|
const [error, setError] = (0, import_react.useState)(null);
|
|
229
301
|
const sessionRef = (0, import_react.useRef)(null);
|
|
230
302
|
const disposedRef = (0, import_react.useRef)(false);
|
|
303
|
+
const messagesRef = (0, import_react.useRef)(messages);
|
|
304
|
+
messagesRef.current = messages;
|
|
231
305
|
const statusRef = (0, import_react.useRef)(status);
|
|
232
306
|
statusRef.current = status;
|
|
233
307
|
const appStateRef = (0, import_react.useRef)(import_react_native.AppState.currentState);
|
|
@@ -239,11 +313,24 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
239
313
|
const onMessageRef = (0, import_react.useRef)(onMessage);
|
|
240
314
|
onMessageRef.current = onMessage;
|
|
241
315
|
const startingRef = (0, import_react.useRef)(false);
|
|
316
|
+
const pendingOptimisticIds = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
242
317
|
const backgroundGraceMs = config.backgroundGraceMs ?? (import_react_native.Platform.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS : 0);
|
|
243
318
|
const callbacks = {
|
|
244
319
|
onMessage: (message) => {
|
|
245
320
|
if (disposedRef.current) return;
|
|
246
|
-
setMessages((prev) =>
|
|
321
|
+
setMessages((prev) => {
|
|
322
|
+
const optimisticIdx = prev.findIndex(
|
|
323
|
+
(m) => pendingOptimisticIds.current.has(m.id) && m.sender_id === message.sender_id && m.body === message.body && m.type === message.type
|
|
324
|
+
);
|
|
325
|
+
if (optimisticIdx !== -1) {
|
|
326
|
+
const optimisticId = prev[optimisticIdx].id;
|
|
327
|
+
pendingOptimisticIds.current.delete(optimisticId);
|
|
328
|
+
const next = [...prev];
|
|
329
|
+
next[optimisticIdx] = message;
|
|
330
|
+
return next;
|
|
331
|
+
}
|
|
332
|
+
return [...prev, message];
|
|
333
|
+
});
|
|
247
334
|
onMessageRef.current?.(message);
|
|
248
335
|
},
|
|
249
336
|
onStatusChange: (nextStatus) => {
|
|
@@ -295,6 +382,13 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
295
382
|
startSession();
|
|
296
383
|
return () => {
|
|
297
384
|
disposedRef.current = true;
|
|
385
|
+
if (statusRef.current === "connected" && sessionRef.current) {
|
|
386
|
+
const lastMsg = messagesRef.current[messagesRef.current.length - 1];
|
|
387
|
+
if (lastMsg) {
|
|
388
|
+
sessionRef.current.markAsRead(lastMsg.id).catch(() => {
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
}
|
|
298
392
|
if (backgroundTimerRef.current) {
|
|
299
393
|
clearTimeout(backgroundTimerRef.current);
|
|
300
394
|
backgroundTimerRef.current = null;
|
|
@@ -351,6 +445,7 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
351
445
|
let optimisticId = null;
|
|
352
446
|
if (optimistic) {
|
|
353
447
|
optimisticId = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
448
|
+
pendingOptimisticIds.current.add(optimisticId);
|
|
354
449
|
const provisionalMsg = {
|
|
355
450
|
id: optimisticId,
|
|
356
451
|
channel_id: channelId,
|
|
@@ -366,15 +461,19 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
366
461
|
try {
|
|
367
462
|
const response = await session.sendMessage(type, body, attributes);
|
|
368
463
|
if (optimistic && optimisticId) {
|
|
369
|
-
|
|
370
|
-
|
|
464
|
+
pendingOptimisticIds.current.delete(optimisticId);
|
|
465
|
+
setMessages((prev) => {
|
|
466
|
+
const stillPending = prev.some((m) => m.id === optimisticId);
|
|
467
|
+
if (!stillPending) return prev;
|
|
468
|
+
return prev.map(
|
|
371
469
|
(m) => m.id === optimisticId ? { ...m, id: response.id, created_at: response.created_at } : m
|
|
372
|
-
)
|
|
373
|
-
);
|
|
470
|
+
);
|
|
471
|
+
});
|
|
374
472
|
}
|
|
375
473
|
return response;
|
|
376
474
|
} catch (err) {
|
|
377
475
|
if (optimistic && optimisticId) {
|
|
476
|
+
pendingOptimisticIds.current.delete(optimisticId);
|
|
378
477
|
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
379
478
|
}
|
|
380
479
|
throw err;
|
|
@@ -389,13 +488,135 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
389
488
|
}, []);
|
|
390
489
|
return { messages, participants, status, error, sendMessage, disconnect };
|
|
391
490
|
}
|
|
491
|
+
|
|
492
|
+
// src/use-unread.ts
|
|
493
|
+
var import_react2 = require("react");
|
|
494
|
+
var import_react_native2 = require("react-native");
|
|
495
|
+
var DEFAULT_BACKGROUND_GRACE_MS2 = 2e3;
|
|
496
|
+
var UNREAD_CUSTOM_EVENTS = ["unread_snapshot", "unread_update", "unread_clear"];
|
|
497
|
+
function useUnread(options) {
|
|
498
|
+
const { config, channelId, participantId, enabled = true } = options;
|
|
499
|
+
const [unreadCount, setUnreadCount] = (0, import_react2.useState)(0);
|
|
500
|
+
const [lastMessageAt, setLastMessageAt] = (0, import_react2.useState)(null);
|
|
501
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
502
|
+
const connRef = (0, import_react2.useRef)(null);
|
|
503
|
+
const configRef = (0, import_react2.useRef)(config);
|
|
504
|
+
configRef.current = config;
|
|
505
|
+
const appStateRef = (0, import_react2.useRef)(import_react_native2.AppState.currentState);
|
|
506
|
+
const backgroundTimerRef = (0, import_react2.useRef)(null);
|
|
507
|
+
const backgroundGraceMs = config.backgroundGraceMs ?? (import_react_native2.Platform.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS2 : 0);
|
|
508
|
+
const connect = (0, import_react2.useCallback)(() => {
|
|
509
|
+
connRef.current?.close();
|
|
510
|
+
connRef.current = null;
|
|
511
|
+
const serviceUrl = resolveServerUrl(configRef.current.manifest, channelId);
|
|
512
|
+
const customHeaders = configRef.current.headers ?? {};
|
|
513
|
+
const url = `${serviceUrl}/channels/${channelId}/unread?participant_id=${encodeURIComponent(participantId)}`;
|
|
514
|
+
connRef.current = createSSEConnection(
|
|
515
|
+
{
|
|
516
|
+
url,
|
|
517
|
+
headers: customHeaders,
|
|
518
|
+
reconnectDelayMs: configRef.current.reconnectDelayMs,
|
|
519
|
+
customEvents: UNREAD_CUSTOM_EVENTS
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
onOpen: () => {
|
|
523
|
+
setError(null);
|
|
524
|
+
},
|
|
525
|
+
onEvent: (eventType, data) => {
|
|
526
|
+
try {
|
|
527
|
+
if (eventType === "unread_snapshot") {
|
|
528
|
+
const snapshot = JSON.parse(data);
|
|
529
|
+
setUnreadCount(snapshot.unread_count);
|
|
530
|
+
setLastMessageAt(snapshot.last_message_at);
|
|
531
|
+
} else if (eventType === "unread_update") {
|
|
532
|
+
const update = JSON.parse(data);
|
|
533
|
+
setUnreadCount((prev) => prev + 1);
|
|
534
|
+
setLastMessageAt(update.created_at);
|
|
535
|
+
} else if (eventType === "unread_clear") {
|
|
536
|
+
const clear = JSON.parse(data);
|
|
537
|
+
setUnreadCount(clear.unread_count);
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
setError(new Error("Failed to parse unread SSE event"));
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
onError: (err) => {
|
|
544
|
+
setError(err);
|
|
545
|
+
},
|
|
546
|
+
onClosed: () => {
|
|
547
|
+
connRef.current = null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
);
|
|
551
|
+
}, [channelId, participantId]);
|
|
552
|
+
const disconnect = (0, import_react2.useCallback)(() => {
|
|
553
|
+
connRef.current?.close();
|
|
554
|
+
connRef.current = null;
|
|
555
|
+
}, []);
|
|
556
|
+
(0, import_react2.useEffect)(() => {
|
|
557
|
+
setUnreadCount(0);
|
|
558
|
+
setLastMessageAt(null);
|
|
559
|
+
setError(null);
|
|
560
|
+
if (!enabled) {
|
|
561
|
+
disconnect();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
connect();
|
|
565
|
+
return () => {
|
|
566
|
+
disconnect();
|
|
567
|
+
if (backgroundTimerRef.current) {
|
|
568
|
+
clearTimeout(backgroundTimerRef.current);
|
|
569
|
+
backgroundTimerRef.current = null;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
}, [channelId, participantId, enabled, connect, disconnect]);
|
|
573
|
+
(0, import_react2.useEffect)(() => {
|
|
574
|
+
if (!enabled) return;
|
|
575
|
+
const subscription = import_react_native2.AppState.addEventListener("change", (nextAppState) => {
|
|
576
|
+
const prev = appStateRef.current;
|
|
577
|
+
appStateRef.current = nextAppState;
|
|
578
|
+
if (!connRef.current && nextAppState !== "active") return;
|
|
579
|
+
const shouldTeardown = nextAppState === "background" || import_react_native2.Platform.OS === "ios" && nextAppState === "inactive";
|
|
580
|
+
if (nextAppState === "active") {
|
|
581
|
+
if (backgroundTimerRef.current) {
|
|
582
|
+
clearTimeout(backgroundTimerRef.current);
|
|
583
|
+
backgroundTimerRef.current = null;
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (prev.match(/inactive|background/) && !connRef.current) {
|
|
587
|
+
connect();
|
|
588
|
+
}
|
|
589
|
+
} else if (shouldTeardown) {
|
|
590
|
+
if (backgroundTimerRef.current) return;
|
|
591
|
+
if (backgroundGraceMs === 0) {
|
|
592
|
+
disconnect();
|
|
593
|
+
} else {
|
|
594
|
+
backgroundTimerRef.current = setTimeout(() => {
|
|
595
|
+
backgroundTimerRef.current = null;
|
|
596
|
+
disconnect();
|
|
597
|
+
}, backgroundGraceMs);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
return () => {
|
|
602
|
+
subscription.remove();
|
|
603
|
+
if (backgroundTimerRef.current) {
|
|
604
|
+
clearTimeout(backgroundTimerRef.current);
|
|
605
|
+
backgroundTimerRef.current = null;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}, [enabled, backgroundGraceMs, connect, disconnect]);
|
|
609
|
+
return { unreadCount, hasUnread: unreadCount > 0, lastMessageAt, error };
|
|
610
|
+
}
|
|
392
611
|
// Annotate the CommonJS export names for ESM import in node:
|
|
393
612
|
0 && (module.exports = {
|
|
394
613
|
ChannelClosedError,
|
|
395
614
|
ChatDisconnectedError,
|
|
396
615
|
createChatSession,
|
|
397
616
|
createManifest,
|
|
617
|
+
createSSEConnection,
|
|
398
618
|
resolveServerUrl,
|
|
399
|
-
useChat
|
|
619
|
+
useChat,
|
|
620
|
+
useUnread
|
|
400
621
|
});
|
|
401
622
|
//# sourceMappingURL=index.js.map
|