@kavchat/react-native 0.0.1 → 0.0.3
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 +38 -0
- package/lib/controller.d.ts +101 -2
- package/lib/controller.js +333 -4
- package/lib/index.d.ts +4 -1
- package/lib/live/api.d.ts +93 -0
- package/lib/live/api.js +41 -0
- package/lib/live/session.d.ts +183 -0
- package/lib/live/session.js +702 -0
- package/lib/live/sse.d.ts +56 -0
- package/lib/live/sse.js +173 -0
- package/lib/live/texts.d.ts +8 -0
- package/lib/live/texts.js +25 -0
- package/lib/ui/KavchatChat.js +20 -8
- package/lib/ui/KavchatProvider.d.ts +11 -0
- package/lib/ui/KavchatProvider.js +16 -2
- package/lib/ui/LiveChatView.d.ts +8 -0
- package/lib/ui/LiveChatView.js +146 -0
- package/package.json +2 -2
- package/src/controller.ts +371 -6
- package/src/index.ts +4 -1
- package/src/live/api.ts +109 -0
- package/src/live/session.ts +784 -0
- package/src/live/sse.ts +196 -0
- package/src/live/texts.ts +25 -0
- package/src/ui/KavchatChat.tsx +37 -8
- package/src/ui/KavchatProvider.tsx +21 -2
- package/src/ui/LiveChatView.tsx +308 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { FetchLike } from '../api';
|
|
2
|
+
/**
|
|
3
|
+
* Client API של צ'אט עם נציג (/v1/client/{publicKey}/live), לפי docs/live-chat-protocol.md.
|
|
4
|
+
* אפליקציה לא צריכה להתחשב ב-CORS, ולכן הגוף נשלח כ-application/json והשפה ב-Accept-Language.
|
|
5
|
+
* הקרדנשל (lvc_...) עובר רק בגוף, אף פעם לא בכתובת, ולא נכתב ללוג
|
|
6
|
+
*/
|
|
7
|
+
import type { LiveAvailability, LiveConversation, LiveEvent, LiveUnavailableReason } from '@kavchat/core';
|
|
8
|
+
export type { LiveAvailability, LiveChatRelease, LiveConversation, LiveEvent, LiveState, LiveUnavailableReason } from '@kavchat/core';
|
|
9
|
+
export type LiveDepartmentAvailability = LiveAvailability['departments'][number];
|
|
10
|
+
export interface LiveSync {
|
|
11
|
+
conversation: LiveConversation;
|
|
12
|
+
events: LiveEvent[];
|
|
13
|
+
hasMore: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface LiveAccepted {
|
|
16
|
+
accepted: true;
|
|
17
|
+
conversation: LiveConversation;
|
|
18
|
+
credential: string;
|
|
19
|
+
credentialExpiresAt: string;
|
|
20
|
+
waitSeconds: number;
|
|
21
|
+
}
|
|
22
|
+
export interface LiveUnavailable {
|
|
23
|
+
accepted: false;
|
|
24
|
+
reason: LiveUnavailableReason;
|
|
25
|
+
fallback: 'ticket';
|
|
26
|
+
}
|
|
27
|
+
export interface LiveRequestBody {
|
|
28
|
+
requestId: string;
|
|
29
|
+
credential?: string;
|
|
30
|
+
identityToken?: string;
|
|
31
|
+
departmentId?: string;
|
|
32
|
+
sessionId?: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
email?: string;
|
|
35
|
+
phone?: string;
|
|
36
|
+
topicId?: string;
|
|
37
|
+
message?: string;
|
|
38
|
+
messageClientId?: string;
|
|
39
|
+
page?: string;
|
|
40
|
+
transcript?: Array<{
|
|
41
|
+
from: 'bot' | 'user';
|
|
42
|
+
text: string;
|
|
43
|
+
at: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
/** תוצאה של קריאה: הצלחה עם data, או קוד שגיאה. status 0 = אין רשת */
|
|
47
|
+
export type LiveResult<T> = {
|
|
48
|
+
ok: true;
|
|
49
|
+
status: number;
|
|
50
|
+
data: T;
|
|
51
|
+
} | {
|
|
52
|
+
ok: false;
|
|
53
|
+
status: number;
|
|
54
|
+
code: string;
|
|
55
|
+
message: string;
|
|
56
|
+
};
|
|
57
|
+
export interface LiveApiConfig {
|
|
58
|
+
apiBase: string;
|
|
59
|
+
publicKey: string;
|
|
60
|
+
getToken: () => string | undefined;
|
|
61
|
+
getLocale: () => string;
|
|
62
|
+
fetch?: FetchLike;
|
|
63
|
+
}
|
|
64
|
+
export declare const liveUrl: (c: LiveApiConfig, path: string) => string;
|
|
65
|
+
/** האם כדאי לנסות שוב: אין רשת, 5xx או 429. שאר ה-4xx סופיים */
|
|
66
|
+
export declare const retryable: (r: {
|
|
67
|
+
ok: boolean;
|
|
68
|
+
status: number;
|
|
69
|
+
}) => boolean;
|
|
70
|
+
export declare const liveApi: {
|
|
71
|
+
availability: (c: LiveApiConfig, departmentId?: string) => Promise<LiveResult<LiveAvailability>>;
|
|
72
|
+
request: (c: LiveApiConfig, body: LiveRequestBody) => Promise<LiveResult<LiveAccepted | LiveUnavailable>>;
|
|
73
|
+
message: (c: LiveApiConfig, credential: string, clientId: string, text: string) => Promise<LiveResult<{
|
|
74
|
+
event: LiveEvent;
|
|
75
|
+
duplicate?: boolean;
|
|
76
|
+
}>>;
|
|
77
|
+
typing: (c: LiveApiConfig, credential: string, on: boolean) => Promise<LiveResult<{
|
|
78
|
+
ok: true;
|
|
79
|
+
}>>;
|
|
80
|
+
sync: (c: LiveApiConfig, credential: string, after: number, wait?: number) => Promise<LiveResult<LiveSync>>;
|
|
81
|
+
streamTicket: (c: LiveApiConfig, credential: string) => Promise<LiveResult<{
|
|
82
|
+
ticket: string;
|
|
83
|
+
expiresIn: number;
|
|
84
|
+
}>>;
|
|
85
|
+
leaveMessage: (c: LiveApiConfig, credential: string, text?: string, clientId?: string) => Promise<LiveResult<{
|
|
86
|
+
conversation: LiveConversation;
|
|
87
|
+
}>>;
|
|
88
|
+
end: (c: LiveApiConfig, credential: string) => Promise<LiveResult<{
|
|
89
|
+
conversation: LiveConversation;
|
|
90
|
+
}>>;
|
|
91
|
+
};
|
|
92
|
+
/** כתובת הזרם. מכילה כרטיס חד-פעמי: לא לכתוב אותה ללוג */
|
|
93
|
+
export declare const streamUrl: (c: LiveApiConfig, ticket: string, after: number) => string;
|
package/lib/live/api.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const liveUrl = (c, path) => `${c.apiBase.replace(/\/+$/, '')}/v1/client/${encodeURIComponent(c.publicKey)}/live/${path}`;
|
|
2
|
+
const doFetch = (c) => c.fetch ?? globalThis.fetch;
|
|
3
|
+
/** האם כדאי לנסות שוב: אין רשת, 5xx או 429. שאר ה-4xx סופיים */
|
|
4
|
+
export const retryable = (r) => !r.ok && (r.status === 0 || r.status === 429 || r.status >= 500);
|
|
5
|
+
async function call(c, method, path, body, auth = false) {
|
|
6
|
+
const headers = { 'Accept-Language': c.getLocale() };
|
|
7
|
+
if (method === 'POST')
|
|
8
|
+
headers['Content-Type'] = 'application/json';
|
|
9
|
+
const token = auth ? c.getToken() : undefined;
|
|
10
|
+
if (token)
|
|
11
|
+
headers.Authorization = `Bearer ${token}`;
|
|
12
|
+
let res;
|
|
13
|
+
try {
|
|
14
|
+
res = await doFetch(c)(liveUrl(c, path), {
|
|
15
|
+
method,
|
|
16
|
+
cache: 'no-store',
|
|
17
|
+
credentials: 'omit',
|
|
18
|
+
headers,
|
|
19
|
+
...(body !== undefined ? { body: JSON.stringify(body) } : {}),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return { ok: false, status: 0, code: 'network', message: '' };
|
|
24
|
+
}
|
|
25
|
+
const json = (await res.json().catch(() => null));
|
|
26
|
+
if (res.ok && json && json.success === true && json.data !== undefined)
|
|
27
|
+
return { ok: true, status: res.status, data: json.data };
|
|
28
|
+
return { ok: false, status: res.ok ? 502 : res.status, code: String(json?.code || `http_${res.status}`), message: String(json?.message || '') };
|
|
29
|
+
}
|
|
30
|
+
export const liveApi = {
|
|
31
|
+
availability: (c, departmentId) => call(c, 'GET', departmentId ? `availability?departmentId=${encodeURIComponent(departmentId)}` : 'availability', undefined, true),
|
|
32
|
+
request: (c, body) => call(c, 'POST', 'requests', body, true),
|
|
33
|
+
message: (c, credential, clientId, text) => call(c, 'POST', 'messages', { credential, clientId, text }),
|
|
34
|
+
typing: (c, credential, on) => call(c, 'POST', 'typing', { credential, on }),
|
|
35
|
+
sync: (c, credential, after, wait = 0) => call(c, 'POST', 'sync', wait > 0 ? { credential, after, wait } : { credential, after }),
|
|
36
|
+
streamTicket: (c, credential) => call(c, 'POST', 'stream-ticket', { credential }),
|
|
37
|
+
leaveMessage: (c, credential, text, clientId) => call(c, 'POST', 'leave-message', text ? { credential, text, clientId } : { credential }),
|
|
38
|
+
end: (c, credential) => call(c, 'POST', 'end', { credential }),
|
|
39
|
+
};
|
|
40
|
+
/** כתובת הזרם. מכילה כרטיס חד-פעמי: לא לכתוב אותה ללוג */
|
|
41
|
+
export const streamUrl = (c, ticket, after) => `${liveUrl(c, 'stream')}?ticket=${encodeURIComponent(ticket)}&after=${Math.max(0, Math.floor(after))}`;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { StorageAdapter } from '../storage';
|
|
2
|
+
import { LiveAccepted, LiveApiConfig, LiveConversation, LiveEvent, LiveRequestBody, LiveResult, LiveUnavailable } from './api';
|
|
3
|
+
import { StreamOpener } from './sse';
|
|
4
|
+
import type { LiveChat, LivePending } from '@kavchat/core';
|
|
5
|
+
export type { LivePending } from '@kavchat/core';
|
|
6
|
+
/**
|
|
7
|
+
* צד המבקר בצ'אט עם נציג: הזרם, חיבור מחדש עם השלמה לפי seq, שליחה אידמפוטנטית,
|
|
8
|
+
* הקלדה, מעבר לרקע ושחזור אחרי הפעלה מחדש. בלי React Native, כדי שאפשר לבדוק ב-Node.
|
|
9
|
+
* הסדר, הסינון הכפול וההודעות שבדרך: המכונה הטהורה ב-@kavchat/core (live.ts), זהה בכל הלקוחות.
|
|
10
|
+
* החוזה: docs/live-chat-protocol.md. הקרדנשל לא נכתב ללוג ולא לכתובת
|
|
11
|
+
*/
|
|
12
|
+
export type LivePhase = 'queued' | 'active' | 'closed' | 'ticket' | 'replaced' | 'expired';
|
|
13
|
+
export type LiveConnection = 'connecting' | 'live' | 'polling' | 'reconnecting' | 'paused' | 'stopped';
|
|
14
|
+
export interface LiveSnapshot {
|
|
15
|
+
phase: LivePhase;
|
|
16
|
+
/** המצב של המכונה המשותפת: מעבירים אותו ל-liveLines ו-liveStatus */
|
|
17
|
+
chat: LiveChat;
|
|
18
|
+
conversation: LiveConversation;
|
|
19
|
+
/** אירועים שהמבקר רואה, ממוינים לפי seq, בלי כפילויות */
|
|
20
|
+
events: LiveEvent[];
|
|
21
|
+
/** הודעות שעוד לא אושרו בשרת */
|
|
22
|
+
pending: LivePending[];
|
|
23
|
+
/** שם הנציג שמקליד, או null */
|
|
24
|
+
agentTyping: string | null;
|
|
25
|
+
connection: LiveConnection;
|
|
26
|
+
waitSeconds: number;
|
|
27
|
+
/** קוד השגיאה האחרונה שצריך להציג (למשל live_message_too_long), או null */
|
|
28
|
+
error: {
|
|
29
|
+
code: string;
|
|
30
|
+
message: string;
|
|
31
|
+
} | null;
|
|
32
|
+
}
|
|
33
|
+
export interface LiveTiming {
|
|
34
|
+
/** השהיות חיבור מחדש וניסיון חוזר: 1, 2, 4, 8, 16, 30 שניות */
|
|
35
|
+
backoffMs: number[];
|
|
36
|
+
/** רעש אקראי של 30% לכל כיוון */
|
|
37
|
+
jitter: number;
|
|
38
|
+
/** בלי שום נתון (גם לא הערת ping) זמן כזה: הפרוקסי ניתק בשקט */
|
|
39
|
+
silenceMs: number;
|
|
40
|
+
typingThrottleMs: number;
|
|
41
|
+
typingHideMs: number;
|
|
42
|
+
pollWaitSeconds: number;
|
|
43
|
+
/** הפסקה מינימלית בין שתי בקשות long-poll שחזרו מיד */
|
|
44
|
+
pollGapMs: number;
|
|
45
|
+
/** כמה זמן להשתמש ב-long-poll אחרי live_busy או כישלונות חוזרים, לפני שמנסים זרם שוב */
|
|
46
|
+
pollModeMs: number;
|
|
47
|
+
/** אחרי queueDeadline: סנכרון אחד, למקרה שהעדכון לא הגיע בזרם */
|
|
48
|
+
deadlineGraceMs: number;
|
|
49
|
+
/** השיחה עדיין queued אחרי הסנכרון (משימת השרת מאחרת): בודקים שוב במרווח הזה, עד 6 פעמים */
|
|
50
|
+
deadlineRetryMs: number;
|
|
51
|
+
sendAttempts: number;
|
|
52
|
+
}
|
|
53
|
+
export declare const DEFAULT_TIMING: LiveTiming;
|
|
54
|
+
/** מה נשמר במכשיר כדי לחזור לשיחה אחרי הפעלה מחדש. הקרדנשל רק כאן ובזיכרון */
|
|
55
|
+
export interface StoredLive {
|
|
56
|
+
credential: string;
|
|
57
|
+
expiresAt: string;
|
|
58
|
+
conversationId: string;
|
|
59
|
+
user: string;
|
|
60
|
+
waitSeconds?: number;
|
|
61
|
+
}
|
|
62
|
+
export declare const liveKey: (publicKey: string) => string;
|
|
63
|
+
export interface LiveSessionOptions {
|
|
64
|
+
api: LiveApiConfig;
|
|
65
|
+
/** אחסון לקרדנשל. עדיף אחסון מאובטח (Keychain / Keystore) */
|
|
66
|
+
store: StorageAdapter;
|
|
67
|
+
storageKey: string;
|
|
68
|
+
/** טביעה של המשתמש המחובר, כדי לא להמשיך שיחה של משתמש אחר */
|
|
69
|
+
user: string;
|
|
70
|
+
/** null: אין זרם, רק long-poll */
|
|
71
|
+
opener?: StreamOpener | null;
|
|
72
|
+
timing?: Partial<LiveTiming>;
|
|
73
|
+
random?: () => number;
|
|
74
|
+
now?: () => number;
|
|
75
|
+
onChange?: (snapshot: LiveSnapshot) => void;
|
|
76
|
+
}
|
|
77
|
+
export declare const isFinalPhase: (p: LivePhase) => boolean;
|
|
78
|
+
export declare function backoffDelay(attempt: number, t: Pick<LiveTiming, 'backoffMs' | 'jitter'>, random?: () => number): number;
|
|
79
|
+
/** בקשת נציג עם ניסיונות חוזרים על רשת, 5xx ו-429. ה-requestId זהה בכל הניסיונות */
|
|
80
|
+
export declare function requestHandoff(api: LiveApiConfig, body: LiveRequestBody, timing?: Partial<LiveTiming>, random?: () => number, attempts?: number): Promise<LiveResult<LiveAccepted | LiveUnavailable>>;
|
|
81
|
+
export declare class LiveSession {
|
|
82
|
+
private o;
|
|
83
|
+
private t;
|
|
84
|
+
private random;
|
|
85
|
+
private opener;
|
|
86
|
+
private credential;
|
|
87
|
+
private expiresAt;
|
|
88
|
+
/** המצב עצמו (שיחה, אירועים, הודעות שבדרך) מנוהל על ידי המכונה הטהורה ב-@kavchat/core */
|
|
89
|
+
private chat;
|
|
90
|
+
/** מצבים סופיים שלא באים מהשרת: הקרדנשל הוחלף (bye replaced) או פג */
|
|
91
|
+
private override;
|
|
92
|
+
private finished;
|
|
93
|
+
private agentTyping;
|
|
94
|
+
private connection;
|
|
95
|
+
private waitSeconds;
|
|
96
|
+
private error;
|
|
97
|
+
private snap;
|
|
98
|
+
/** עולה בכל עצירה (רקע, סיום, dispose), כדי שלולאות ישנות ייעצרו אחרי await */
|
|
99
|
+
private gen;
|
|
100
|
+
private conn;
|
|
101
|
+
private attempt;
|
|
102
|
+
private failuresWithoutReady;
|
|
103
|
+
private pollUntil;
|
|
104
|
+
private polling;
|
|
105
|
+
private paused;
|
|
106
|
+
private disposed;
|
|
107
|
+
private typingOn;
|
|
108
|
+
private lastTypingAt;
|
|
109
|
+
private armedDeadline;
|
|
110
|
+
private timers;
|
|
111
|
+
private get phase();
|
|
112
|
+
private get lastSeq();
|
|
113
|
+
private constructor();
|
|
114
|
+
/** אחרי accepted: שומר את הקרדנשל, מציג המתנה ופותח את הזרם */
|
|
115
|
+
static start(o: LiveSessionOptions, accepted: LiveAccepted): Promise<LiveSession>;
|
|
116
|
+
/**
|
|
117
|
+
* אחרי הפעלה מחדש: אם יש קרדנשל בתוקף של אותו משתמש, בונה את השיחה מ-sync after 0 וממשיך.
|
|
118
|
+
* שיחה שהסתיימה בינתיים או קרדנשל שפג: מוחק ומחזיר null
|
|
119
|
+
*/
|
|
120
|
+
static restore(o: LiveSessionOptions): Promise<LiveSession | null>;
|
|
121
|
+
getSnapshot: () => LiveSnapshot;
|
|
122
|
+
isFinal(): boolean;
|
|
123
|
+
private build;
|
|
124
|
+
private emit;
|
|
125
|
+
private now;
|
|
126
|
+
private canRun;
|
|
127
|
+
private setConnection;
|
|
128
|
+
/** מחליף את המצב בתוצאה של המכונה הטהורה, ומטפל בתוצאות הצדדיות: מועדים, הקלדה, סיום */
|
|
129
|
+
private setChat;
|
|
130
|
+
private afterChange;
|
|
131
|
+
/** אובייקט השיחה הוא מקור האמת. closed ו-ticket סופיים: המכונה לא חוזרת מהם */
|
|
132
|
+
private applyConversation;
|
|
133
|
+
/** כפילות לפי seq מתעלמים, מכניסים לפי הסדר, ולא מחכים ל-seq חסר (liveApplyEvent) */
|
|
134
|
+
private applyEvent;
|
|
135
|
+
private applySync;
|
|
136
|
+
/** POST /sync עד hasMore=false. false כשנכשל */
|
|
137
|
+
private syncAll;
|
|
138
|
+
private run;
|
|
139
|
+
private connect;
|
|
140
|
+
private onMessage;
|
|
141
|
+
private setTyping;
|
|
142
|
+
private onStreamFailure;
|
|
143
|
+
private scheduleReconnect;
|
|
144
|
+
private dropStream;
|
|
145
|
+
private armSilence;
|
|
146
|
+
/** long-poll: כשאין זרם בסביבה, אחרי live_busy, או כשהזרם נכשל שוב ושוב. בלי הקלדה */
|
|
147
|
+
private poll;
|
|
148
|
+
private pollResolve;
|
|
149
|
+
private wait;
|
|
150
|
+
private armDeadline;
|
|
151
|
+
/** השרת הופך את השיחה לפנייה בתום ההמתנה. אם העדכון לא הגיע בזרם (או ברקע), הסנכרון מביא אותו */
|
|
152
|
+
private checkDeadline;
|
|
153
|
+
private armExpiry;
|
|
154
|
+
private expire;
|
|
155
|
+
private end;
|
|
156
|
+
/** מצב סופי: עוצרים הכל ומוחקים את הקרדנשל מהמכשיר */
|
|
157
|
+
private finish;
|
|
158
|
+
private clearTimer;
|
|
159
|
+
/** מעבר לרקע: סוגרים את הזרם. ההודעות שבדרך ממשיכות לנסות */
|
|
160
|
+
pause(): void;
|
|
161
|
+
/** חזרה לקדמה: השלמה של מה שהוחמץ ואז זרם חדש */
|
|
162
|
+
resume(): Promise<void>;
|
|
163
|
+
/** עוצר בלי למחוק את הקרדנשל (למשל כשהאפליקציה נסגרת), כדי שאפשר יהיה לשחזר */
|
|
164
|
+
dispose(): void;
|
|
165
|
+
/** עוצר ומוחק את הקרדנשל (החלפת משתמש) */
|
|
166
|
+
forget(): void;
|
|
167
|
+
/** מחזיר קוד שגיאה, או null כשההודעה נוספה לתור השליחה */
|
|
168
|
+
send(text: string): string | null;
|
|
169
|
+
/** שליחה חוזרת של הודעה שנכשלה, עם אותו clientId */
|
|
170
|
+
retry(clientId: string): void;
|
|
171
|
+
/** null: מסיר את הבועה */
|
|
172
|
+
private setPending;
|
|
173
|
+
private deliver;
|
|
174
|
+
/** נקרא בכל שינוי בשדה. on: true לכל היותר כל 3 שניות, on: false כשהשדה מתרוקן. בלי ניסיונות חוזרים */
|
|
175
|
+
typing(text: string): void;
|
|
176
|
+
private stopTyping;
|
|
177
|
+
/** "להשאיר הודעה במקום": השיחה הופכת לפנייה */
|
|
178
|
+
leaveMessage(text?: string): Promise<boolean>;
|
|
179
|
+
/** סיום השיחה. בטוח לחזור עליו */
|
|
180
|
+
endChat(): Promise<boolean>;
|
|
181
|
+
private command;
|
|
182
|
+
clearError(): void;
|
|
183
|
+
}
|