@pedi/chika-sdk 1.0.0
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 +56 -0
- package/dist/index.d.mts +94 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +401 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +359 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @pedi/chika-sdk
|
|
2
|
+
|
|
3
|
+
React Native SDK for real-time chat over Server-Sent Events (SSE).
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Provides a drop-in React hook (`useChat`) that connects your React Native app to a Chika chat server. Handles the entire chat lifecycle — joining a channel, streaming messages in real-time, sending messages, reconnecting on network loss, and cleaning up when the component unmounts or the app backgrounds.
|
|
8
|
+
|
|
9
|
+
## Problems It Solves
|
|
10
|
+
|
|
11
|
+
- **SSE in React Native** — Wraps `react-native-sse` with proper lifecycle management so you don't have to manually open/close connections
|
|
12
|
+
- **Reconnection and gap-fill** — Automatically reconnects when the network drops and replays missed messages using `Last-Event-ID`, so conversations never have gaps
|
|
13
|
+
- **AppState-aware lifecycle** — Tears down connections when the app backgrounds and reconnects when it returns, with platform-specific handling (Android grace period for keyboard/dialog triggers)
|
|
14
|
+
- **Message deduplication** — Prevents duplicate messages from SSE echo and reconnection replays
|
|
15
|
+
- **Optimistic UI** — Messages appear in the local list instantly on send, before server confirmation
|
|
16
|
+
- **Type-safe chat domains** — Full generic support via `ChatDomain` so message types, roles, and attributes are enforced at compile time
|
|
17
|
+
|
|
18
|
+
## Key Features
|
|
19
|
+
|
|
20
|
+
- `useChat<D>()` React hook with full TypeScript generics
|
|
21
|
+
- `createChatSession<D>()` imperative API for non-React usage
|
|
22
|
+
- Automatic SSE reconnection with configurable delay
|
|
23
|
+
- Platform-aware AppState handling (iOS vs Android)
|
|
24
|
+
- Optimistic message sending with deduplication
|
|
25
|
+
- Hash-based bucket routing for multi-server deployments
|
|
26
|
+
- Custom error classes (`ChatDisconnectedError`, `ChannelClosedError`)
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { useChat, createManifest } from '@pedi/chika-sdk';
|
|
32
|
+
import type { PediChat } from '@pedi/chika-types';
|
|
33
|
+
|
|
34
|
+
function ChatScreen({ bookingId, user }) {
|
|
35
|
+
const { messages, status, sendMessage } = useChat<PediChat>({
|
|
36
|
+
config: {
|
|
37
|
+
manifest: createManifest('https://chat.example.com'),
|
|
38
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
39
|
+
},
|
|
40
|
+
channelId: `booking_${bookingId}`,
|
|
41
|
+
profile: { id: user.id, role: 'rider', name: user.name },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await sendMessage('chat', 'Hello!', { device: 'ios' });
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Peer dependencies:** `react >= 18`, `react-native >= 0.72`
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
See the [docs](./docs/) for detailed documentation:
|
|
53
|
+
|
|
54
|
+
- [API Reference](./docs/api-reference.md) — `useChat`, `createChatSession`, utilities, error classes, and all config options
|
|
55
|
+
- [Guides](./docs/guides.md) — Connection lifecycle, AppState handling, reconnection, deduplication, custom domains
|
|
56
|
+
- [AI Agent Integration Guide](./docs/llm-integration-guide.md) — Patterns, recipes, pitfalls, and checklists for LLM-assisted implementation
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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';
|
|
3
|
+
|
|
4
|
+
type ChatStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'closed' | 'error';
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for the chat SDK.
|
|
7
|
+
*
|
|
8
|
+
* @property manifest - Bucket routing manifest for server URL resolution.
|
|
9
|
+
* @property headers - Custom headers applied to all HTTP and SSE requests (e.g., auth tokens).
|
|
10
|
+
* @property reconnectDelayMs - Delay before SSE reconnection attempt. Default: 3000ms.
|
|
11
|
+
* @property backgroundGraceMs - Grace period before teardown on app background. Default: 2000ms on Android, 0ms on iOS.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Configuration for the chat SDK.
|
|
15
|
+
*
|
|
16
|
+
* @property manifest - Bucket routing manifest for server URL resolution.
|
|
17
|
+
* @property headers - Custom headers applied to all HTTP and SSE requests (e.g., auth tokens).
|
|
18
|
+
* @property reconnectDelayMs - Delay before SSE reconnection attempt. Default: 3000ms.
|
|
19
|
+
* @property backgroundGraceMs - Grace period before teardown on app background. Default: 2000ms on Android, 0ms on iOS.
|
|
20
|
+
* @property optimisticSend - Append messages to the local array immediately on send. Default: true.
|
|
21
|
+
*/
|
|
22
|
+
interface ChatConfig {
|
|
23
|
+
manifest: ChatManifest;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
reconnectDelayMs?: number;
|
|
26
|
+
backgroundGraceMs?: number;
|
|
27
|
+
optimisticSend?: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface UseChatOptions<D extends ChatDomain = DefaultDomain> {
|
|
30
|
+
config: ChatConfig;
|
|
31
|
+
channelId: string;
|
|
32
|
+
profile: Participant<D>;
|
|
33
|
+
onMessage?: (message: Message<D>) => void;
|
|
34
|
+
}
|
|
35
|
+
interface UseChatReturn<D extends ChatDomain = DefaultDomain> {
|
|
36
|
+
messages: Message<D>[];
|
|
37
|
+
participants: Participant<D>[];
|
|
38
|
+
status: ChatStatus;
|
|
39
|
+
error: Error | null;
|
|
40
|
+
sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;
|
|
41
|
+
disconnect: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* React hook for real-time chat over SSE.
|
|
46
|
+
* Manages connection lifecycle, AppState transitions, message deduplication, and reconnection.
|
|
47
|
+
*
|
|
48
|
+
* @template D - Chat domain type for role/message type narrowing. Defaults to DefaultDomain.
|
|
49
|
+
*/
|
|
50
|
+
declare function useChat<D extends ChatDomain = DefaultDomain>({ config, channelId, profile, onMessage }: UseChatOptions<D>): UseChatReturn<D>;
|
|
51
|
+
|
|
52
|
+
interface SessionCallbacks<D extends ChatDomain = DefaultDomain> {
|
|
53
|
+
onMessage: (message: Message<D>) => void;
|
|
54
|
+
onStatusChange: (status: ChatStatus) => void;
|
|
55
|
+
onError: (error: Error) => void;
|
|
56
|
+
onResync: () => void;
|
|
57
|
+
}
|
|
58
|
+
interface ChatSession<D extends ChatDomain = DefaultDomain> {
|
|
59
|
+
serviceUrl: string;
|
|
60
|
+
channelId: string;
|
|
61
|
+
initialParticipants: Participant<D>[];
|
|
62
|
+
initialMessages: Message<D>[];
|
|
63
|
+
sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;
|
|
64
|
+
disconnect: () => void;
|
|
65
|
+
}
|
|
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
|
+
declare function createChatSession<D extends ChatDomain = DefaultDomain>(config: ChatConfig, channelId: string, profile: Participant<D>, callbacks: SessionCallbacks<D>): Promise<ChatSession<D>>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates a single-server manifest. Use this when all channels route to the same server.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const config: ChatConfig = { manifest: createManifest('https://chat.example.com') };
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
declare function createManifest(serverUrl: string): ChatManifest;
|
|
83
|
+
declare function resolveServerUrl(manifest: ChatManifest, channelId: string): string;
|
|
84
|
+
|
|
85
|
+
declare class ChatDisconnectedError extends Error {
|
|
86
|
+
readonly status: ChatStatus;
|
|
87
|
+
constructor(status: ChatStatus);
|
|
88
|
+
}
|
|
89
|
+
declare class ChannelClosedError extends Error {
|
|
90
|
+
readonly channelId: string;
|
|
91
|
+
constructor(channelId: string);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { ChannelClosedError, type ChatConfig, ChatDisconnectedError, type ChatSession, type ChatStatus, type SessionCallbacks, type UseChatOptions, type UseChatReturn, createChatSession, createManifest, resolveServerUrl, useChat };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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';
|
|
3
|
+
|
|
4
|
+
type ChatStatus = 'connecting' | 'connected' | 'reconnecting' | 'disconnected' | 'closed' | 'error';
|
|
5
|
+
/**
|
|
6
|
+
* Configuration for the chat SDK.
|
|
7
|
+
*
|
|
8
|
+
* @property manifest - Bucket routing manifest for server URL resolution.
|
|
9
|
+
* @property headers - Custom headers applied to all HTTP and SSE requests (e.g., auth tokens).
|
|
10
|
+
* @property reconnectDelayMs - Delay before SSE reconnection attempt. Default: 3000ms.
|
|
11
|
+
* @property backgroundGraceMs - Grace period before teardown on app background. Default: 2000ms on Android, 0ms on iOS.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Configuration for the chat SDK.
|
|
15
|
+
*
|
|
16
|
+
* @property manifest - Bucket routing manifest for server URL resolution.
|
|
17
|
+
* @property headers - Custom headers applied to all HTTP and SSE requests (e.g., auth tokens).
|
|
18
|
+
* @property reconnectDelayMs - Delay before SSE reconnection attempt. Default: 3000ms.
|
|
19
|
+
* @property backgroundGraceMs - Grace period before teardown on app background. Default: 2000ms on Android, 0ms on iOS.
|
|
20
|
+
* @property optimisticSend - Append messages to the local array immediately on send. Default: true.
|
|
21
|
+
*/
|
|
22
|
+
interface ChatConfig {
|
|
23
|
+
manifest: ChatManifest;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
reconnectDelayMs?: number;
|
|
26
|
+
backgroundGraceMs?: number;
|
|
27
|
+
optimisticSend?: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface UseChatOptions<D extends ChatDomain = DefaultDomain> {
|
|
30
|
+
config: ChatConfig;
|
|
31
|
+
channelId: string;
|
|
32
|
+
profile: Participant<D>;
|
|
33
|
+
onMessage?: (message: Message<D>) => void;
|
|
34
|
+
}
|
|
35
|
+
interface UseChatReturn<D extends ChatDomain = DefaultDomain> {
|
|
36
|
+
messages: Message<D>[];
|
|
37
|
+
participants: Participant<D>[];
|
|
38
|
+
status: ChatStatus;
|
|
39
|
+
error: Error | null;
|
|
40
|
+
sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;
|
|
41
|
+
disconnect: () => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* React hook for real-time chat over SSE.
|
|
46
|
+
* Manages connection lifecycle, AppState transitions, message deduplication, and reconnection.
|
|
47
|
+
*
|
|
48
|
+
* @template D - Chat domain type for role/message type narrowing. Defaults to DefaultDomain.
|
|
49
|
+
*/
|
|
50
|
+
declare function useChat<D extends ChatDomain = DefaultDomain>({ config, channelId, profile, onMessage }: UseChatOptions<D>): UseChatReturn<D>;
|
|
51
|
+
|
|
52
|
+
interface SessionCallbacks<D extends ChatDomain = DefaultDomain> {
|
|
53
|
+
onMessage: (message: Message<D>) => void;
|
|
54
|
+
onStatusChange: (status: ChatStatus) => void;
|
|
55
|
+
onError: (error: Error) => void;
|
|
56
|
+
onResync: () => void;
|
|
57
|
+
}
|
|
58
|
+
interface ChatSession<D extends ChatDomain = DefaultDomain> {
|
|
59
|
+
serviceUrl: string;
|
|
60
|
+
channelId: string;
|
|
61
|
+
initialParticipants: Participant<D>[];
|
|
62
|
+
initialMessages: Message<D>[];
|
|
63
|
+
sendMessage: (type: D['messageType'], body: string, attributes?: MessageAttributes<D>) => Promise<SendMessageResponse>;
|
|
64
|
+
disconnect: () => void;
|
|
65
|
+
}
|
|
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
|
+
declare function createChatSession<D extends ChatDomain = DefaultDomain>(config: ChatConfig, channelId: string, profile: Participant<D>, callbacks: SessionCallbacks<D>): Promise<ChatSession<D>>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates a single-server manifest. Use this when all channels route to the same server.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const config: ChatConfig = { manifest: createManifest('https://chat.example.com') };
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
declare function createManifest(serverUrl: string): ChatManifest;
|
|
83
|
+
declare function resolveServerUrl(manifest: ChatManifest, channelId: string): string;
|
|
84
|
+
|
|
85
|
+
declare class ChatDisconnectedError extends Error {
|
|
86
|
+
readonly status: ChatStatus;
|
|
87
|
+
constructor(status: ChatStatus);
|
|
88
|
+
}
|
|
89
|
+
declare class ChannelClosedError extends Error {
|
|
90
|
+
readonly channelId: string;
|
|
91
|
+
constructor(channelId: string);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { ChannelClosedError, type ChatConfig, ChatDisconnectedError, type ChatSession, type ChatStatus, type SessionCallbacks, type UseChatOptions, type UseChatReturn, createChatSession, createManifest, resolveServerUrl, useChat };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ChannelClosedError: () => ChannelClosedError,
|
|
34
|
+
ChatDisconnectedError: () => ChatDisconnectedError,
|
|
35
|
+
createChatSession: () => createChatSession,
|
|
36
|
+
createManifest: () => createManifest,
|
|
37
|
+
resolveServerUrl: () => resolveServerUrl,
|
|
38
|
+
useChat: () => useChat
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
|
|
42
|
+
// src/use-chat.ts
|
|
43
|
+
var import_react = require("react");
|
|
44
|
+
var import_react_native = require("react-native");
|
|
45
|
+
|
|
46
|
+
// src/errors.ts
|
|
47
|
+
var ChatDisconnectedError = class extends Error {
|
|
48
|
+
constructor(status) {
|
|
49
|
+
super(`Cannot send message while ${status}`);
|
|
50
|
+
this.status = status;
|
|
51
|
+
this.name = "ChatDisconnectedError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
var ChannelClosedError = class extends Error {
|
|
55
|
+
constructor(channelId) {
|
|
56
|
+
super(`Channel ${channelId} is closed`);
|
|
57
|
+
this.channelId = channelId;
|
|
58
|
+
this.name = "ChannelClosedError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/session.ts
|
|
63
|
+
var import_react_native_sse = __toESM(require("react-native-sse"));
|
|
64
|
+
|
|
65
|
+
// src/resolve-url.ts
|
|
66
|
+
function createManifest(serverUrl) {
|
|
67
|
+
return { buckets: [{ group: "default", range: [0, 99], server_url: serverUrl }] };
|
|
68
|
+
}
|
|
69
|
+
function resolveServerUrl(manifest, channelId) {
|
|
70
|
+
const hash = [...channelId].reduce((sum, c) => sum + c.charCodeAt(0), 0) % 100;
|
|
71
|
+
const bucket = manifest.buckets.find(
|
|
72
|
+
(b) => hash >= b.range[0] && hash <= b.range[1]
|
|
73
|
+
);
|
|
74
|
+
if (!bucket) throw new Error(`No chat bucket for hash ${hash}`);
|
|
75
|
+
return bucket.server_url;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/session.ts
|
|
79
|
+
var DEFAULT_RECONNECT_DELAY_MS = 3e3;
|
|
80
|
+
var MAX_SEEN_IDS = 500;
|
|
81
|
+
async function createChatSession(config, channelId, profile, callbacks) {
|
|
82
|
+
const serviceUrl = resolveServerUrl(config.manifest, channelId);
|
|
83
|
+
const customHeaders = config.headers ?? {};
|
|
84
|
+
const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;
|
|
85
|
+
callbacks.onStatusChange("connecting");
|
|
86
|
+
const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {
|
|
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));
|
|
101
|
+
let es = null;
|
|
102
|
+
let disposed = false;
|
|
103
|
+
let reconnectTimer = null;
|
|
104
|
+
function trimSeenIds() {
|
|
105
|
+
if (seenMessageIds.size <= MAX_SEEN_IDS) return;
|
|
106
|
+
const ids = [...seenMessageIds];
|
|
107
|
+
seenMessageIds.clear();
|
|
108
|
+
for (const id of ids.slice(-MAX_SEEN_IDS)) {
|
|
109
|
+
seenMessageIds.add(id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function connect() {
|
|
113
|
+
if (disposed) return;
|
|
114
|
+
const streamUrl = lastEventId ? `${serviceUrl}/channels/${channelId}/stream` : `${serviceUrl}/channels/${channelId}/stream?since_time=${encodeURIComponent(joinedAt)}`;
|
|
115
|
+
es = new import_react_native_sse.default(streamUrl, {
|
|
116
|
+
headers: {
|
|
117
|
+
...customHeaders,
|
|
118
|
+
...lastEventId && { "Last-Event-ID": lastEventId }
|
|
119
|
+
},
|
|
120
|
+
pollingInterval: 0
|
|
121
|
+
});
|
|
122
|
+
es.addEventListener("open", () => {
|
|
123
|
+
if (disposed) return;
|
|
124
|
+
callbacks.onStatusChange("connected");
|
|
125
|
+
});
|
|
126
|
+
es.addEventListener("message", (event) => {
|
|
127
|
+
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
|
+
if (event.lastEventId) {
|
|
136
|
+
lastEventId = event.lastEventId;
|
|
137
|
+
}
|
|
138
|
+
if (seenMessageIds.has(message.id)) return;
|
|
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();
|
|
147
|
+
});
|
|
148
|
+
es.addEventListener("error", (event) => {
|
|
149
|
+
if (disposed) return;
|
|
150
|
+
const msg = "message" in event ? String(event.message) : "";
|
|
151
|
+
if (msg.includes("Channel is closed") || msg.includes("410")) {
|
|
152
|
+
callbacks.onStatusChange("closed");
|
|
153
|
+
cleanupEventSource();
|
|
154
|
+
disposed = true;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (msg) callbacks.onError(new Error(msg));
|
|
158
|
+
scheduleReconnect();
|
|
159
|
+
});
|
|
160
|
+
es.addEventListener("close", () => {
|
|
161
|
+
if (disposed) return;
|
|
162
|
+
scheduleReconnect();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function scheduleReconnect() {
|
|
166
|
+
if (disposed || reconnectTimer) return;
|
|
167
|
+
callbacks.onStatusChange("reconnecting");
|
|
168
|
+
cleanupEventSource();
|
|
169
|
+
reconnectTimer = setTimeout(() => {
|
|
170
|
+
reconnectTimer = null;
|
|
171
|
+
connect();
|
|
172
|
+
}, reconnectDelay);
|
|
173
|
+
}
|
|
174
|
+
function cleanupEventSource() {
|
|
175
|
+
if (es) {
|
|
176
|
+
es.removeAllEventListeners();
|
|
177
|
+
es.close();
|
|
178
|
+
es = null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
connect();
|
|
182
|
+
return {
|
|
183
|
+
serviceUrl,
|
|
184
|
+
channelId,
|
|
185
|
+
initialParticipants: participants,
|
|
186
|
+
initialMessages: messages,
|
|
187
|
+
sendMessage: async (type, body, attributes) => {
|
|
188
|
+
if (disposed) throw new ChatDisconnectedError("disconnected");
|
|
189
|
+
const payload = {
|
|
190
|
+
sender_id: profile.id,
|
|
191
|
+
type,
|
|
192
|
+
body,
|
|
193
|
+
attributes
|
|
194
|
+
};
|
|
195
|
+
const res = await fetch(
|
|
196
|
+
`${serviceUrl}/channels/${channelId}/messages`,
|
|
197
|
+
{
|
|
198
|
+
method: "POST",
|
|
199
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
200
|
+
body: JSON.stringify(payload)
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
throw new Error(`Send failed: ${res.status} ${await res.text()}`);
|
|
205
|
+
}
|
|
206
|
+
const response = await res.json();
|
|
207
|
+
seenMessageIds.add(response.id);
|
|
208
|
+
return response;
|
|
209
|
+
},
|
|
210
|
+
disconnect: () => {
|
|
211
|
+
disposed = true;
|
|
212
|
+
if (reconnectTimer) {
|
|
213
|
+
clearTimeout(reconnectTimer);
|
|
214
|
+
reconnectTimer = null;
|
|
215
|
+
}
|
|
216
|
+
cleanupEventSource();
|
|
217
|
+
callbacks.onStatusChange("disconnected");
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/use-chat.ts
|
|
223
|
+
var DEFAULT_BACKGROUND_GRACE_MS = 2e3;
|
|
224
|
+
function useChat({ config, channelId, profile, onMessage }) {
|
|
225
|
+
const [messages, setMessages] = (0, import_react.useState)([]);
|
|
226
|
+
const [participants, setParticipants] = (0, import_react.useState)([]);
|
|
227
|
+
const [status, setStatus] = (0, import_react.useState)("connecting");
|
|
228
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
229
|
+
const sessionRef = (0, import_react.useRef)(null);
|
|
230
|
+
const disposedRef = (0, import_react.useRef)(false);
|
|
231
|
+
const statusRef = (0, import_react.useRef)(status);
|
|
232
|
+
statusRef.current = status;
|
|
233
|
+
const appStateRef = (0, import_react.useRef)(import_react_native.AppState.currentState);
|
|
234
|
+
const backgroundTimerRef = (0, import_react.useRef)(null);
|
|
235
|
+
const profileRef = (0, import_react.useRef)(profile);
|
|
236
|
+
profileRef.current = profile;
|
|
237
|
+
const configRef = (0, import_react.useRef)(config);
|
|
238
|
+
configRef.current = config;
|
|
239
|
+
const onMessageRef = (0, import_react.useRef)(onMessage);
|
|
240
|
+
onMessageRef.current = onMessage;
|
|
241
|
+
const startingRef = (0, import_react.useRef)(false);
|
|
242
|
+
const backgroundGraceMs = config.backgroundGraceMs ?? (import_react_native.Platform.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS : 0);
|
|
243
|
+
const callbacks = {
|
|
244
|
+
onMessage: (message) => {
|
|
245
|
+
if (disposedRef.current) return;
|
|
246
|
+
setMessages((prev) => [...prev, message]);
|
|
247
|
+
onMessageRef.current?.(message);
|
|
248
|
+
},
|
|
249
|
+
onStatusChange: (nextStatus) => {
|
|
250
|
+
if (disposedRef.current) return;
|
|
251
|
+
setStatus(nextStatus);
|
|
252
|
+
if (nextStatus === "connected") setError(null);
|
|
253
|
+
},
|
|
254
|
+
onError: (err) => {
|
|
255
|
+
if (disposedRef.current) return;
|
|
256
|
+
setError(err);
|
|
257
|
+
},
|
|
258
|
+
onResync: () => {
|
|
259
|
+
if (disposedRef.current) return;
|
|
260
|
+
startSession();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
async function startSession() {
|
|
264
|
+
if (startingRef.current) return;
|
|
265
|
+
startingRef.current = true;
|
|
266
|
+
const existing = sessionRef.current;
|
|
267
|
+
if (existing) {
|
|
268
|
+
existing.disconnect();
|
|
269
|
+
sessionRef.current = null;
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const session = await createChatSession(configRef.current, channelId, profileRef.current, callbacks);
|
|
273
|
+
if (disposedRef.current) {
|
|
274
|
+
session.disconnect();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
sessionRef.current = session;
|
|
278
|
+
setParticipants(session.initialParticipants);
|
|
279
|
+
setMessages(session.initialMessages);
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (disposedRef.current) return;
|
|
282
|
+
if (err instanceof ChannelClosedError) {
|
|
283
|
+
setStatus("closed");
|
|
284
|
+
setError(err);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
setStatus("error");
|
|
288
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
289
|
+
} finally {
|
|
290
|
+
startingRef.current = false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
(0, import_react.useEffect)(() => {
|
|
294
|
+
disposedRef.current = false;
|
|
295
|
+
startSession();
|
|
296
|
+
return () => {
|
|
297
|
+
disposedRef.current = true;
|
|
298
|
+
if (backgroundTimerRef.current) {
|
|
299
|
+
clearTimeout(backgroundTimerRef.current);
|
|
300
|
+
backgroundTimerRef.current = null;
|
|
301
|
+
}
|
|
302
|
+
sessionRef.current?.disconnect();
|
|
303
|
+
sessionRef.current = null;
|
|
304
|
+
};
|
|
305
|
+
}, [channelId]);
|
|
306
|
+
(0, import_react.useEffect)(() => {
|
|
307
|
+
function teardownSession() {
|
|
308
|
+
sessionRef.current?.disconnect();
|
|
309
|
+
sessionRef.current = null;
|
|
310
|
+
setStatus("disconnected");
|
|
311
|
+
}
|
|
312
|
+
const subscription = import_react_native.AppState.addEventListener("change", (nextAppState) => {
|
|
313
|
+
const prev = appStateRef.current;
|
|
314
|
+
appStateRef.current = nextAppState;
|
|
315
|
+
if (!sessionRef.current && nextAppState !== "active") return;
|
|
316
|
+
const shouldTeardown = nextAppState === "background" || import_react_native.Platform.OS === "ios" && nextAppState === "inactive";
|
|
317
|
+
if (nextAppState === "active") {
|
|
318
|
+
if (backgroundTimerRef.current) {
|
|
319
|
+
clearTimeout(backgroundTimerRef.current);
|
|
320
|
+
backgroundTimerRef.current = null;
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (prev.match(/inactive|background/) && !sessionRef.current) {
|
|
324
|
+
startSession();
|
|
325
|
+
}
|
|
326
|
+
} else if (shouldTeardown) {
|
|
327
|
+
if (backgroundTimerRef.current) return;
|
|
328
|
+
if (backgroundGraceMs === 0) {
|
|
329
|
+
teardownSession();
|
|
330
|
+
} else {
|
|
331
|
+
backgroundTimerRef.current = setTimeout(() => {
|
|
332
|
+
backgroundTimerRef.current = null;
|
|
333
|
+
teardownSession();
|
|
334
|
+
}, backgroundGraceMs);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
return () => {
|
|
339
|
+
subscription.remove();
|
|
340
|
+
if (backgroundTimerRef.current) {
|
|
341
|
+
clearTimeout(backgroundTimerRef.current);
|
|
342
|
+
backgroundTimerRef.current = null;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}, [channelId, backgroundGraceMs]);
|
|
346
|
+
const sendMessage = (0, import_react.useCallback)(
|
|
347
|
+
async (type, body, attributes) => {
|
|
348
|
+
const session = sessionRef.current;
|
|
349
|
+
if (!session) throw new ChatDisconnectedError(statusRef.current);
|
|
350
|
+
const optimistic = configRef.current.optimisticSend !== false;
|
|
351
|
+
let optimisticId = null;
|
|
352
|
+
if (optimistic) {
|
|
353
|
+
optimisticId = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
354
|
+
const provisionalMsg = {
|
|
355
|
+
id: optimisticId,
|
|
356
|
+
channel_id: channelId,
|
|
357
|
+
sender_id: profileRef.current.id,
|
|
358
|
+
sender_role: profileRef.current.role,
|
|
359
|
+
type,
|
|
360
|
+
body,
|
|
361
|
+
attributes: attributes ?? {},
|
|
362
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
363
|
+
};
|
|
364
|
+
setMessages((prev) => [...prev, provisionalMsg]);
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
const response = await session.sendMessage(type, body, attributes);
|
|
368
|
+
if (optimistic && optimisticId) {
|
|
369
|
+
setMessages(
|
|
370
|
+
(prev) => prev.map(
|
|
371
|
+
(m) => m.id === optimisticId ? { ...m, id: response.id, created_at: response.created_at } : m
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
return response;
|
|
376
|
+
} catch (err) {
|
|
377
|
+
if (optimistic && optimisticId) {
|
|
378
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
379
|
+
}
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
[channelId]
|
|
384
|
+
);
|
|
385
|
+
const disconnect = (0, import_react.useCallback)(() => {
|
|
386
|
+
sessionRef.current?.disconnect();
|
|
387
|
+
sessionRef.current = null;
|
|
388
|
+
setStatus("disconnected");
|
|
389
|
+
}, []);
|
|
390
|
+
return { messages, participants, status, error, sendMessage, disconnect };
|
|
391
|
+
}
|
|
392
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
393
|
+
0 && (module.exports = {
|
|
394
|
+
ChannelClosedError,
|
|
395
|
+
ChatDisconnectedError,
|
|
396
|
+
createChatSession,
|
|
397
|
+
createManifest,
|
|
398
|
+
resolveServerUrl,
|
|
399
|
+
useChat
|
|
400
|
+
});
|
|
401
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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\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>[]) => [...prev, message]);\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 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 setMessages((prev) =>\n 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 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"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// src/use-chat.ts
|
|
2
|
+
import { useEffect, useRef, useState, useCallback } from "react";
|
|
3
|
+
import { AppState, Platform } from "react-native";
|
|
4
|
+
|
|
5
|
+
// src/errors.ts
|
|
6
|
+
var ChatDisconnectedError = class extends Error {
|
|
7
|
+
constructor(status) {
|
|
8
|
+
super(`Cannot send message while ${status}`);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.name = "ChatDisconnectedError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var ChannelClosedError = class extends Error {
|
|
14
|
+
constructor(channelId) {
|
|
15
|
+
super(`Channel ${channelId} is closed`);
|
|
16
|
+
this.channelId = channelId;
|
|
17
|
+
this.name = "ChannelClosedError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/session.ts
|
|
22
|
+
import EventSource from "react-native-sse";
|
|
23
|
+
|
|
24
|
+
// src/resolve-url.ts
|
|
25
|
+
function createManifest(serverUrl) {
|
|
26
|
+
return { buckets: [{ group: "default", range: [0, 99], server_url: serverUrl }] };
|
|
27
|
+
}
|
|
28
|
+
function resolveServerUrl(manifest, channelId) {
|
|
29
|
+
const hash = [...channelId].reduce((sum, c) => sum + c.charCodeAt(0), 0) % 100;
|
|
30
|
+
const bucket = manifest.buckets.find(
|
|
31
|
+
(b) => hash >= b.range[0] && hash <= b.range[1]
|
|
32
|
+
);
|
|
33
|
+
if (!bucket) throw new Error(`No chat bucket for hash ${hash}`);
|
|
34
|
+
return bucket.server_url;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/session.ts
|
|
38
|
+
var DEFAULT_RECONNECT_DELAY_MS = 3e3;
|
|
39
|
+
var MAX_SEEN_IDS = 500;
|
|
40
|
+
async function createChatSession(config, channelId, profile, callbacks) {
|
|
41
|
+
const serviceUrl = resolveServerUrl(config.manifest, channelId);
|
|
42
|
+
const customHeaders = config.headers ?? {};
|
|
43
|
+
const reconnectDelay = config.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;
|
|
44
|
+
callbacks.onStatusChange("connecting");
|
|
45
|
+
const joinRes = await fetch(`${serviceUrl}/channels/${channelId}/join`, {
|
|
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));
|
|
60
|
+
let es = null;
|
|
61
|
+
let disposed = false;
|
|
62
|
+
let reconnectTimer = null;
|
|
63
|
+
function trimSeenIds() {
|
|
64
|
+
if (seenMessageIds.size <= MAX_SEEN_IDS) return;
|
|
65
|
+
const ids = [...seenMessageIds];
|
|
66
|
+
seenMessageIds.clear();
|
|
67
|
+
for (const id of ids.slice(-MAX_SEEN_IDS)) {
|
|
68
|
+
seenMessageIds.add(id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function connect() {
|
|
72
|
+
if (disposed) return;
|
|
73
|
+
const streamUrl = lastEventId ? `${serviceUrl}/channels/${channelId}/stream` : `${serviceUrl}/channels/${channelId}/stream?since_time=${encodeURIComponent(joinedAt)}`;
|
|
74
|
+
es = new EventSource(streamUrl, {
|
|
75
|
+
headers: {
|
|
76
|
+
...customHeaders,
|
|
77
|
+
...lastEventId && { "Last-Event-ID": lastEventId }
|
|
78
|
+
},
|
|
79
|
+
pollingInterval: 0
|
|
80
|
+
});
|
|
81
|
+
es.addEventListener("open", () => {
|
|
82
|
+
if (disposed) return;
|
|
83
|
+
callbacks.onStatusChange("connected");
|
|
84
|
+
});
|
|
85
|
+
es.addEventListener("message", (event) => {
|
|
86
|
+
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
|
+
if (event.lastEventId) {
|
|
95
|
+
lastEventId = event.lastEventId;
|
|
96
|
+
}
|
|
97
|
+
if (seenMessageIds.has(message.id)) return;
|
|
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();
|
|
106
|
+
});
|
|
107
|
+
es.addEventListener("error", (event) => {
|
|
108
|
+
if (disposed) return;
|
|
109
|
+
const msg = "message" in event ? String(event.message) : "";
|
|
110
|
+
if (msg.includes("Channel is closed") || msg.includes("410")) {
|
|
111
|
+
callbacks.onStatusChange("closed");
|
|
112
|
+
cleanupEventSource();
|
|
113
|
+
disposed = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (msg) callbacks.onError(new Error(msg));
|
|
117
|
+
scheduleReconnect();
|
|
118
|
+
});
|
|
119
|
+
es.addEventListener("close", () => {
|
|
120
|
+
if (disposed) return;
|
|
121
|
+
scheduleReconnect();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function scheduleReconnect() {
|
|
125
|
+
if (disposed || reconnectTimer) return;
|
|
126
|
+
callbacks.onStatusChange("reconnecting");
|
|
127
|
+
cleanupEventSource();
|
|
128
|
+
reconnectTimer = setTimeout(() => {
|
|
129
|
+
reconnectTimer = null;
|
|
130
|
+
connect();
|
|
131
|
+
}, reconnectDelay);
|
|
132
|
+
}
|
|
133
|
+
function cleanupEventSource() {
|
|
134
|
+
if (es) {
|
|
135
|
+
es.removeAllEventListeners();
|
|
136
|
+
es.close();
|
|
137
|
+
es = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
connect();
|
|
141
|
+
return {
|
|
142
|
+
serviceUrl,
|
|
143
|
+
channelId,
|
|
144
|
+
initialParticipants: participants,
|
|
145
|
+
initialMessages: messages,
|
|
146
|
+
sendMessage: async (type, body, attributes) => {
|
|
147
|
+
if (disposed) throw new ChatDisconnectedError("disconnected");
|
|
148
|
+
const payload = {
|
|
149
|
+
sender_id: profile.id,
|
|
150
|
+
type,
|
|
151
|
+
body,
|
|
152
|
+
attributes
|
|
153
|
+
};
|
|
154
|
+
const res = await fetch(
|
|
155
|
+
`${serviceUrl}/channels/${channelId}/messages`,
|
|
156
|
+
{
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: { "Content-Type": "application/json", ...customHeaders },
|
|
159
|
+
body: JSON.stringify(payload)
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
throw new Error(`Send failed: ${res.status} ${await res.text()}`);
|
|
164
|
+
}
|
|
165
|
+
const response = await res.json();
|
|
166
|
+
seenMessageIds.add(response.id);
|
|
167
|
+
return response;
|
|
168
|
+
},
|
|
169
|
+
disconnect: () => {
|
|
170
|
+
disposed = true;
|
|
171
|
+
if (reconnectTimer) {
|
|
172
|
+
clearTimeout(reconnectTimer);
|
|
173
|
+
reconnectTimer = null;
|
|
174
|
+
}
|
|
175
|
+
cleanupEventSource();
|
|
176
|
+
callbacks.onStatusChange("disconnected");
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/use-chat.ts
|
|
182
|
+
var DEFAULT_BACKGROUND_GRACE_MS = 2e3;
|
|
183
|
+
function useChat({ config, channelId, profile, onMessage }) {
|
|
184
|
+
const [messages, setMessages] = useState([]);
|
|
185
|
+
const [participants, setParticipants] = useState([]);
|
|
186
|
+
const [status, setStatus] = useState("connecting");
|
|
187
|
+
const [error, setError] = useState(null);
|
|
188
|
+
const sessionRef = useRef(null);
|
|
189
|
+
const disposedRef = useRef(false);
|
|
190
|
+
const statusRef = useRef(status);
|
|
191
|
+
statusRef.current = status;
|
|
192
|
+
const appStateRef = useRef(AppState.currentState);
|
|
193
|
+
const backgroundTimerRef = useRef(null);
|
|
194
|
+
const profileRef = useRef(profile);
|
|
195
|
+
profileRef.current = profile;
|
|
196
|
+
const configRef = useRef(config);
|
|
197
|
+
configRef.current = config;
|
|
198
|
+
const onMessageRef = useRef(onMessage);
|
|
199
|
+
onMessageRef.current = onMessage;
|
|
200
|
+
const startingRef = useRef(false);
|
|
201
|
+
const backgroundGraceMs = config.backgroundGraceMs ?? (Platform.OS === "android" ? DEFAULT_BACKGROUND_GRACE_MS : 0);
|
|
202
|
+
const callbacks = {
|
|
203
|
+
onMessage: (message) => {
|
|
204
|
+
if (disposedRef.current) return;
|
|
205
|
+
setMessages((prev) => [...prev, message]);
|
|
206
|
+
onMessageRef.current?.(message);
|
|
207
|
+
},
|
|
208
|
+
onStatusChange: (nextStatus) => {
|
|
209
|
+
if (disposedRef.current) return;
|
|
210
|
+
setStatus(nextStatus);
|
|
211
|
+
if (nextStatus === "connected") setError(null);
|
|
212
|
+
},
|
|
213
|
+
onError: (err) => {
|
|
214
|
+
if (disposedRef.current) return;
|
|
215
|
+
setError(err);
|
|
216
|
+
},
|
|
217
|
+
onResync: () => {
|
|
218
|
+
if (disposedRef.current) return;
|
|
219
|
+
startSession();
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
async function startSession() {
|
|
223
|
+
if (startingRef.current) return;
|
|
224
|
+
startingRef.current = true;
|
|
225
|
+
const existing = sessionRef.current;
|
|
226
|
+
if (existing) {
|
|
227
|
+
existing.disconnect();
|
|
228
|
+
sessionRef.current = null;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const session = await createChatSession(configRef.current, channelId, profileRef.current, callbacks);
|
|
232
|
+
if (disposedRef.current) {
|
|
233
|
+
session.disconnect();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
sessionRef.current = session;
|
|
237
|
+
setParticipants(session.initialParticipants);
|
|
238
|
+
setMessages(session.initialMessages);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
if (disposedRef.current) return;
|
|
241
|
+
if (err instanceof ChannelClosedError) {
|
|
242
|
+
setStatus("closed");
|
|
243
|
+
setError(err);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
setStatus("error");
|
|
247
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
248
|
+
} finally {
|
|
249
|
+
startingRef.current = false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
disposedRef.current = false;
|
|
254
|
+
startSession();
|
|
255
|
+
return () => {
|
|
256
|
+
disposedRef.current = true;
|
|
257
|
+
if (backgroundTimerRef.current) {
|
|
258
|
+
clearTimeout(backgroundTimerRef.current);
|
|
259
|
+
backgroundTimerRef.current = null;
|
|
260
|
+
}
|
|
261
|
+
sessionRef.current?.disconnect();
|
|
262
|
+
sessionRef.current = null;
|
|
263
|
+
};
|
|
264
|
+
}, [channelId]);
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
function teardownSession() {
|
|
267
|
+
sessionRef.current?.disconnect();
|
|
268
|
+
sessionRef.current = null;
|
|
269
|
+
setStatus("disconnected");
|
|
270
|
+
}
|
|
271
|
+
const subscription = AppState.addEventListener("change", (nextAppState) => {
|
|
272
|
+
const prev = appStateRef.current;
|
|
273
|
+
appStateRef.current = nextAppState;
|
|
274
|
+
if (!sessionRef.current && nextAppState !== "active") return;
|
|
275
|
+
const shouldTeardown = nextAppState === "background" || Platform.OS === "ios" && nextAppState === "inactive";
|
|
276
|
+
if (nextAppState === "active") {
|
|
277
|
+
if (backgroundTimerRef.current) {
|
|
278
|
+
clearTimeout(backgroundTimerRef.current);
|
|
279
|
+
backgroundTimerRef.current = null;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (prev.match(/inactive|background/) && !sessionRef.current) {
|
|
283
|
+
startSession();
|
|
284
|
+
}
|
|
285
|
+
} else if (shouldTeardown) {
|
|
286
|
+
if (backgroundTimerRef.current) return;
|
|
287
|
+
if (backgroundGraceMs === 0) {
|
|
288
|
+
teardownSession();
|
|
289
|
+
} else {
|
|
290
|
+
backgroundTimerRef.current = setTimeout(() => {
|
|
291
|
+
backgroundTimerRef.current = null;
|
|
292
|
+
teardownSession();
|
|
293
|
+
}, backgroundGraceMs);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
return () => {
|
|
298
|
+
subscription.remove();
|
|
299
|
+
if (backgroundTimerRef.current) {
|
|
300
|
+
clearTimeout(backgroundTimerRef.current);
|
|
301
|
+
backgroundTimerRef.current = null;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}, [channelId, backgroundGraceMs]);
|
|
305
|
+
const sendMessage = useCallback(
|
|
306
|
+
async (type, body, attributes) => {
|
|
307
|
+
const session = sessionRef.current;
|
|
308
|
+
if (!session) throw new ChatDisconnectedError(statusRef.current);
|
|
309
|
+
const optimistic = configRef.current.optimisticSend !== false;
|
|
310
|
+
let optimisticId = null;
|
|
311
|
+
if (optimistic) {
|
|
312
|
+
optimisticId = `optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
313
|
+
const provisionalMsg = {
|
|
314
|
+
id: optimisticId,
|
|
315
|
+
channel_id: channelId,
|
|
316
|
+
sender_id: profileRef.current.id,
|
|
317
|
+
sender_role: profileRef.current.role,
|
|
318
|
+
type,
|
|
319
|
+
body,
|
|
320
|
+
attributes: attributes ?? {},
|
|
321
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
322
|
+
};
|
|
323
|
+
setMessages((prev) => [...prev, provisionalMsg]);
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const response = await session.sendMessage(type, body, attributes);
|
|
327
|
+
if (optimistic && optimisticId) {
|
|
328
|
+
setMessages(
|
|
329
|
+
(prev) => prev.map(
|
|
330
|
+
(m) => m.id === optimisticId ? { ...m, id: response.id, created_at: response.created_at } : m
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
return response;
|
|
335
|
+
} catch (err) {
|
|
336
|
+
if (optimistic && optimisticId) {
|
|
337
|
+
setMessages((prev) => prev.filter((m) => m.id !== optimisticId));
|
|
338
|
+
}
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
[channelId]
|
|
343
|
+
);
|
|
344
|
+
const disconnect = useCallback(() => {
|
|
345
|
+
sessionRef.current?.disconnect();
|
|
346
|
+
sessionRef.current = null;
|
|
347
|
+
setStatus("disconnected");
|
|
348
|
+
}, []);
|
|
349
|
+
return { messages, participants, status, error, sendMessage, disconnect };
|
|
350
|
+
}
|
|
351
|
+
export {
|
|
352
|
+
ChannelClosedError,
|
|
353
|
+
ChatDisconnectedError,
|
|
354
|
+
createChatSession,
|
|
355
|
+
createManifest,
|
|
356
|
+
resolveServerUrl,
|
|
357
|
+
useChat
|
|
358
|
+
};
|
|
359
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/use-chat.ts","../src/errors.ts","../src/session.ts","../src/resolve-url.ts"],"sourcesContent":["import { useEffect, useRef, useState, useCallback } from 'react';\nimport { AppState, Platform, type AppStateStatus } from 'react-native';\nimport type {\n ChatDomain,\n DefaultDomain,\n Message,\n Participant,\n MessageAttributes,\n SendMessageResponse,\n} from '@pedi/chika-types';\nimport type { UseChatOptions, UseChatReturn, ChatStatus } from './types';\nimport { ChatDisconnectedError, ChannelClosedError } from './errors';\nimport { createChatSession, type ChatSession, type SessionCallbacks } from './session';\n\nconst DEFAULT_BACKGROUND_GRACE_MS = 2000;\n\n/**\n * React hook for real-time chat over SSE.\n * Manages connection lifecycle, AppState transitions, message deduplication, and reconnection.\n *\n * @template D - Chat domain type for role/message type narrowing. Defaults to DefaultDomain.\n */\nexport function useChat<D extends ChatDomain = DefaultDomain>(\n { config, channelId, profile, onMessage }: UseChatOptions<D>,\n): UseChatReturn<D> {\n const [messages, setMessages] = useState<Message<D>[]>([]);\n const [participants, setParticipants] = useState<Participant<D>[]>([]);\n const [status, setStatus] = useState<ChatStatus>('connecting');\n const [error, setError] = useState<Error | null>(null);\n\n const sessionRef = useRef<ChatSession<D> | null>(null);\n const disposedRef = useRef(false);\n const 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\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>[]) => [...prev, message]);\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 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 setMessages((prev) =>\n 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 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":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pedi/chika-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React Native SDK for Pedi Chika chat service",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"dev": "tsup --watch",
|
|
28
|
+
"typecheck": "tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@pedi/chika-types": "^1.0.0",
|
|
32
|
+
"react-native-sse": "^1.2.1"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=18",
|
|
36
|
+
"react-native": ">=0.72"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^19.2.14",
|
|
40
|
+
"tsup": "^8.4.0",
|
|
41
|
+
"typescript": "^5.7.0"
|
|
42
|
+
}
|
|
43
|
+
}
|