@pedi/chika-sdk 1.0.2 → 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 +278 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +275 -74
- 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);
|
|
@@ -308,6 +382,13 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
308
382
|
startSession();
|
|
309
383
|
return () => {
|
|
310
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
|
+
}
|
|
311
392
|
if (backgroundTimerRef.current) {
|
|
312
393
|
clearTimeout(backgroundTimerRef.current);
|
|
313
394
|
backgroundTimerRef.current = null;
|
|
@@ -407,13 +488,135 @@ function useChat({ config, channelId, profile, onMessage }) {
|
|
|
407
488
|
}, []);
|
|
408
489
|
return { messages, participants, status, error, sendMessage, disconnect };
|
|
409
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
|
+
}
|
|
410
611
|
// Annotate the CommonJS export names for ESM import in node:
|
|
411
612
|
0 && (module.exports = {
|
|
412
613
|
ChannelClosedError,
|
|
413
614
|
ChatDisconnectedError,
|
|
414
615
|
createChatSession,
|
|
415
616
|
createManifest,
|
|
617
|
+
createSSEConnection,
|
|
416
618
|
resolveServerUrl,
|
|
417
|
-
useChat
|
|
619
|
+
useChat,
|
|
620
|
+
useUnread
|
|
418
621
|
});
|
|
419
622
|
//# sourceMappingURL=index.js.map
|