@servicetitan/titan-chat-ui-common 7.1.2 → 9.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/CHANGELOG.md +25 -0
- package/dist/hooks/use-customization-chat.js +2 -1
- package/dist/hooks/use-customization-chat.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/models/__mocks__/support-chat.mock.js +13 -12
- package/dist/models/__mocks__/support-chat.mock.js.map +1 -1
- package/dist/models/chat-customizations.js +2 -1
- package/dist/models/chat-customizations.js.map +1 -1
- package/dist/models/file-descriptor.js +2 -1
- package/dist/models/file-descriptor.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +1 -2
- package/dist/models/index.js.map +1 -1
- package/dist/models/support-chat.js +37 -41
- package/dist/models/support-chat.js.map +1 -1
- package/dist/stores/__tests__/chat-ui.store.test.js +117 -106
- package/dist/stores/__tests__/chat-ui.store.test.js.map +1 -1
- package/dist/stores/chat-ui-backend-echo.store.js +86 -87
- package/dist/stores/chat-ui-backend-echo.store.js.map +1 -1
- package/dist/stores/chat-ui-backend.store.js +1 -0
- package/dist/stores/chat-ui-backend.store.js.map +1 -1
- package/dist/stores/chat-ui.store.d.ts +2 -2
- package/dist/stores/chat-ui.store.d.ts.map +1 -1
- package/dist/stores/chat-ui.store.js +307 -329
- package/dist/stores/chat-ui.store.js.map +1 -1
- package/dist/stores/index.d.ts +4 -2
- package/dist/stores/index.d.ts.map +1 -1
- package/dist/stores/index.js +2 -1
- package/dist/stores/index.js.map +1 -1
- package/dist/streaming/__tests__/chat-sse-client-lifecycle.test.d.ts +2 -0
- package/dist/streaming/__tests__/chat-sse-client-lifecycle.test.d.ts.map +1 -0
- package/dist/streaming/__tests__/chat-sse-client-lifecycle.test.js +86 -0
- package/dist/streaming/__tests__/chat-sse-client-lifecycle.test.js.map +1 -0
- package/dist/streaming/__tests__/chat-sse-client-resilience.test.d.ts +2 -0
- package/dist/streaming/__tests__/chat-sse-client-resilience.test.d.ts.map +1 -0
- package/dist/streaming/__tests__/chat-sse-client-resilience.test.js +140 -0
- package/dist/streaming/__tests__/chat-sse-client-resilience.test.js.map +1 -0
- package/dist/streaming/__tests__/chat-sse-client.test.d.ts +2 -0
- package/dist/streaming/__tests__/chat-sse-client.test.d.ts.map +1 -0
- package/dist/streaming/__tests__/chat-sse-client.test.js +209 -0
- package/dist/streaming/__tests__/chat-sse-client.test.js.map +1 -0
- package/dist/streaming/__tests__/mock-wiring.test.d.ts +2 -0
- package/dist/streaming/__tests__/mock-wiring.test.d.ts.map +1 -0
- package/dist/streaming/__tests__/mock-wiring.test.js +20 -0
- package/dist/streaming/__tests__/mock-wiring.test.js.map +1 -0
- package/dist/streaming/__tests__/streaming-progress.model.test.d.ts +2 -0
- package/dist/streaming/__tests__/streaming-progress.model.test.d.ts.map +1 -0
- package/dist/streaming/__tests__/streaming-progress.model.test.js +89 -0
- package/dist/streaming/__tests__/streaming-progress.model.test.js.map +1 -0
- package/dist/streaming/chat-sse-client.d.ts +75 -0
- package/dist/streaming/chat-sse-client.d.ts.map +1 -0
- package/dist/streaming/chat-sse-client.js +189 -0
- package/dist/streaming/chat-sse-client.js.map +1 -0
- package/dist/streaming/index.d.ts +3 -0
- package/dist/streaming/index.d.ts.map +1 -0
- package/dist/streaming/index.js +4 -0
- package/dist/streaming/index.js.map +1 -0
- package/dist/streaming/streaming-progress.model.d.ts +39 -0
- package/dist/streaming/streaming-progress.model.d.ts.map +1 -0
- package/dist/streaming/streaming-progress.model.js +69 -0
- package/dist/streaming/streaming-progress.model.js.map +1 -0
- package/dist/utils/__tests__/text-utils.test.js +33 -14
- package/dist/utils/__tests__/text-utils.test.js.map +1 -1
- package/dist/utils/test-utils.js +5 -5
- package/dist/utils/test-utils.js.map +1 -1
- package/dist/utils/text-utils.js +12 -12
- package/dist/utils/text-utils.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +1 -0
- package/src/models/index.ts +2 -2
- package/src/stores/chat-ui.store.ts +1 -3
- package/src/stores/index.ts +3 -3
- package/src/streaming/__tests__/chat-sse-client-lifecycle.test.ts +67 -0
- package/src/streaming/__tests__/chat-sse-client-resilience.test.ts +141 -0
- package/src/streaming/__tests__/chat-sse-client.test.ts +152 -0
- package/src/streaming/__tests__/mock-wiring.test.ts +25 -0
- package/src/streaming/__tests__/streaming-progress.model.test.ts +72 -0
- package/src/streaming/chat-sse-client.ts +236 -0
- package/src/streaming/index.ts +2 -0
- package/src/streaming/streaming-progress.model.ts +80 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect } from '@jest/globals';
|
|
2
|
-
import { extractFilenameAndExt, formatChatMessageDate, getFirstName, getNameInitials, getNameInitialsFirst
|
|
3
|
-
describe('text-utils', ()
|
|
4
|
-
test('should get initials from name', ()
|
|
2
|
+
import { extractFilenameAndExt, formatChatMessageDate, getFirstName, getNameInitials, getNameInitialsFirst } from '../text-utils';
|
|
3
|
+
describe('text-utils', ()=>{
|
|
4
|
+
test('should get initials from name', ()=>{
|
|
5
5
|
expect(getNameInitials('')).toEqual('');
|
|
6
6
|
expect(getNameInitials('A')).toEqual('A');
|
|
7
7
|
expect(getNameInitials('AB')).toEqual('A');
|
|
@@ -11,15 +11,33 @@ describe('text-utils', () => {
|
|
|
11
11
|
expect(getNameInitials('abcd ef')).toEqual('AE');
|
|
12
12
|
expect(getNameInitials('a b c d')).toEqual('AB');
|
|
13
13
|
});
|
|
14
|
-
test('should extract filename and ext', ()
|
|
15
|
-
expect(extractFilenameAndExt('')).toEqual([
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
expect(extractFilenameAndExt('a
|
|
20
|
-
|
|
14
|
+
test('should extract filename and ext', ()=>{
|
|
15
|
+
expect(extractFilenameAndExt('')).toEqual([
|
|
16
|
+
'',
|
|
17
|
+
''
|
|
18
|
+
]);
|
|
19
|
+
expect(extractFilenameAndExt('a')).toEqual([
|
|
20
|
+
'a',
|
|
21
|
+
''
|
|
22
|
+
]);
|
|
23
|
+
expect(extractFilenameAndExt('a.')).toEqual([
|
|
24
|
+
'a.',
|
|
25
|
+
''
|
|
26
|
+
]);
|
|
27
|
+
expect(extractFilenameAndExt('a.b')).toEqual([
|
|
28
|
+
'a.',
|
|
29
|
+
'b'
|
|
30
|
+
]);
|
|
31
|
+
expect(extractFilenameAndExt('a.b.c')).toEqual([
|
|
32
|
+
'a.b.',
|
|
33
|
+
'c'
|
|
34
|
+
]);
|
|
35
|
+
expect(extractFilenameAndExt('aaaa.bbbb.cccc')).toEqual([
|
|
36
|
+
'aaaa.bbbb.',
|
|
37
|
+
'cccc'
|
|
38
|
+
]);
|
|
21
39
|
});
|
|
22
|
-
test('should getFirstName', ()
|
|
40
|
+
test('should getFirstName', ()=>{
|
|
23
41
|
expect(getFirstName('')).toEqual('');
|
|
24
42
|
expect(getFirstName('A')).toEqual('A');
|
|
25
43
|
expect(getFirstName('AB')).toEqual('AB');
|
|
@@ -29,7 +47,7 @@ describe('text-utils', () => {
|
|
|
29
47
|
expect(getFirstName('abcd ef')).toEqual('abcd');
|
|
30
48
|
expect(getFirstName('a b c d')).toEqual('a');
|
|
31
49
|
});
|
|
32
|
-
test('should getNameInitialsFirst', ()
|
|
50
|
+
test('should getNameInitialsFirst', ()=>{
|
|
33
51
|
expect(getNameInitials('')).toEqual('');
|
|
34
52
|
expect(getNameInitials('A')).toEqual('A');
|
|
35
53
|
expect(getNameInitials('AB')).toEqual('A');
|
|
@@ -39,13 +57,13 @@ describe('text-utils', () => {
|
|
|
39
57
|
expect(getNameInitials('abcd ef')).toEqual('AE');
|
|
40
58
|
expect(getNameInitials('a b c d')).toEqual('AB');
|
|
41
59
|
});
|
|
42
|
-
test('should formatChatMessageDate', ()
|
|
60
|
+
test('should formatChatMessageDate', ()=>{
|
|
43
61
|
const date = new Date('2023-10-01T12:00:00Z');
|
|
44
62
|
expect(formatChatMessageDate(date)).toEqual('12:00 PM');
|
|
45
63
|
expect(formatChatMessageDate(new Date('2023-10-01T00:00:00Z'))).toEqual('12:00 AM');
|
|
46
64
|
expect(formatChatMessageDate(new Date('2023-10-01T23:59:59Z'))).toEqual('11:59 PM');
|
|
47
65
|
});
|
|
48
|
-
test('should getNameInitialsFirst', ()
|
|
66
|
+
test('should getNameInitialsFirst', ()=>{
|
|
49
67
|
expect(getNameInitialsFirst('')).toEqual('');
|
|
50
68
|
expect(getNameInitialsFirst('A')).toEqual('A');
|
|
51
69
|
expect(getNameInitialsFirst('AB')).toEqual('A');
|
|
@@ -56,4 +74,5 @@ describe('text-utils', () => {
|
|
|
56
74
|
expect(getNameInitialsFirst('a b c d')).toEqual('A');
|
|
57
75
|
});
|
|
58
76
|
});
|
|
77
|
+
|
|
59
78
|
//# sourceMappingURL=text-utils.test.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"
|
|
1
|
+
{"version":3,"sources":["../../../src/utils/__tests__/text-utils.test.ts"],"sourcesContent":["import { expect } from '@jest/globals';\nimport {\n extractFilenameAndExt,\n formatChatMessageDate,\n getFirstName,\n getNameInitials,\n getNameInitialsFirst,\n} from '../text-utils';\n\ndescribe('text-utils', () => {\n test('should get initials from name', () => {\n expect(getNameInitials('')).toEqual('');\n expect(getNameInitials('A')).toEqual('A');\n expect(getNameInitials('AB')).toEqual('A');\n expect(getNameInitials('abcd')).toEqual('A');\n expect(getNameInitials(' abcd ')).toEqual('A');\n expect(getNameInitials(' a b c ')).toEqual('AB');\n expect(getNameInitials('abcd ef')).toEqual('AE');\n expect(getNameInitials('a b c d')).toEqual('AB');\n });\n\n test('should extract filename and ext', () => {\n expect(extractFilenameAndExt('')).toEqual(['', '']);\n expect(extractFilenameAndExt('a')).toEqual(['a', '']);\n expect(extractFilenameAndExt('a.')).toEqual(['a.', '']);\n expect(extractFilenameAndExt('a.b')).toEqual(['a.', 'b']);\n expect(extractFilenameAndExt('a.b.c')).toEqual(['a.b.', 'c']);\n expect(extractFilenameAndExt('aaaa.bbbb.cccc')).toEqual(['aaaa.bbbb.', 'cccc']);\n });\n\n test('should getFirstName', () => {\n expect(getFirstName('')).toEqual('');\n expect(getFirstName('A')).toEqual('A');\n expect(getFirstName('AB')).toEqual('AB');\n expect(getFirstName('abcd')).toEqual('abcd');\n expect(getFirstName(' abcd ')).toEqual('abcd');\n expect(getFirstName(' a b c ')).toEqual('a');\n expect(getFirstName('abcd ef')).toEqual('abcd');\n expect(getFirstName('a b c d')).toEqual('a');\n });\n\n test('should getNameInitialsFirst', () => {\n expect(getNameInitials('')).toEqual('');\n expect(getNameInitials('A')).toEqual('A');\n expect(getNameInitials('AB')).toEqual('A');\n expect(getNameInitials('abcd')).toEqual('A');\n expect(getNameInitials(' abcd ')).toEqual('A');\n expect(getNameInitials(' a b c ')).toEqual('AB');\n expect(getNameInitials('abcd ef')).toEqual('AE');\n expect(getNameInitials('a b c d')).toEqual('AB');\n });\n\n test('should formatChatMessageDate', () => {\n const date = new Date('2023-10-01T12:00:00Z');\n expect(formatChatMessageDate(date)).toEqual('12:00 PM');\n expect(formatChatMessageDate(new Date('2023-10-01T00:00:00Z'))).toEqual('12:00 AM');\n expect(formatChatMessageDate(new Date('2023-10-01T23:59:59Z'))).toEqual('11:59 PM');\n });\n\n test('should getNameInitialsFirst', () => {\n expect(getNameInitialsFirst('')).toEqual('');\n expect(getNameInitialsFirst('A')).toEqual('A');\n expect(getNameInitialsFirst('AB')).toEqual('A');\n expect(getNameInitialsFirst('abcd')).toEqual('A');\n expect(getNameInitialsFirst(' abcd ')).toEqual('A');\n expect(getNameInitialsFirst(' a b c ')).toEqual('A');\n expect(getNameInitialsFirst('abcd ef')).toEqual('A');\n expect(getNameInitialsFirst('a b c d')).toEqual('A');\n });\n});\n"],"names":["expect","extractFilenameAndExt","formatChatMessageDate","getFirstName","getNameInitials","getNameInitialsFirst","describe","test","toEqual","date","Date"],"mappings":"AAAA,SAASA,MAAM,QAAQ,gBAAgB;AACvC,SACIC,qBAAqB,EACrBC,qBAAqB,EACrBC,YAAY,EACZC,eAAe,EACfC,oBAAoB,QACjB,gBAAgB;AAEvBC,SAAS,cAAc;IACnBC,KAAK,iCAAiC;QAClCP,OAAOI,gBAAgB,KAAKI,OAAO,CAAC;QACpCR,OAAOI,gBAAgB,MAAMI,OAAO,CAAC;QACrCR,OAAOI,gBAAgB,OAAOI,OAAO,CAAC;QACtCR,OAAOI,gBAAgB,SAASI,OAAO,CAAC;QACxCR,OAAOI,gBAAgB,cAAcI,OAAO,CAAC;QAC7CR,OAAOI,gBAAgB,gBAAgBI,OAAO,CAAC;QAC/CR,OAAOI,gBAAgB,YAAYI,OAAO,CAAC;QAC3CR,OAAOI,gBAAgB,YAAYI,OAAO,CAAC;IAC/C;IAEAD,KAAK,mCAAmC;QACpCP,OAAOC,sBAAsB,KAAKO,OAAO,CAAC;YAAC;YAAI;SAAG;QAClDR,OAAOC,sBAAsB,MAAMO,OAAO,CAAC;YAAC;YAAK;SAAG;QACpDR,OAAOC,sBAAsB,OAAOO,OAAO,CAAC;YAAC;YAAM;SAAG;QACtDR,OAAOC,sBAAsB,QAAQO,OAAO,CAAC;YAAC;YAAM;SAAI;QACxDR,OAAOC,sBAAsB,UAAUO,OAAO,CAAC;YAAC;YAAQ;SAAI;QAC5DR,OAAOC,sBAAsB,mBAAmBO,OAAO,CAAC;YAAC;YAAc;SAAO;IAClF;IAEAD,KAAK,uBAAuB;QACxBP,OAAOG,aAAa,KAAKK,OAAO,CAAC;QACjCR,OAAOG,aAAa,MAAMK,OAAO,CAAC;QAClCR,OAAOG,aAAa,OAAOK,OAAO,CAAC;QACnCR,OAAOG,aAAa,SAASK,OAAO,CAAC;QACrCR,OAAOG,aAAa,cAAcK,OAAO,CAAC;QAC1CR,OAAOG,aAAa,gBAAgBK,OAAO,CAAC;QAC5CR,OAAOG,aAAa,YAAYK,OAAO,CAAC;QACxCR,OAAOG,aAAa,YAAYK,OAAO,CAAC;IAC5C;IAEAD,KAAK,+BAA+B;QAChCP,OAAOI,gBAAgB,KAAKI,OAAO,CAAC;QACpCR,OAAOI,gBAAgB,MAAMI,OAAO,CAAC;QACrCR,OAAOI,gBAAgB,OAAOI,OAAO,CAAC;QACtCR,OAAOI,gBAAgB,SAASI,OAAO,CAAC;QACxCR,OAAOI,gBAAgB,cAAcI,OAAO,CAAC;QAC7CR,OAAOI,gBAAgB,gBAAgBI,OAAO,CAAC;QAC/CR,OAAOI,gBAAgB,YAAYI,OAAO,CAAC;QAC3CR,OAAOI,gBAAgB,YAAYI,OAAO,CAAC;IAC/C;IAEAD,KAAK,gCAAgC;QACjC,MAAME,OAAO,IAAIC,KAAK;QACtBV,OAAOE,sBAAsBO,OAAOD,OAAO,CAAC;QAC5CR,OAAOE,sBAAsB,IAAIQ,KAAK,0BAA0BF,OAAO,CAAC;QACxER,OAAOE,sBAAsB,IAAIQ,KAAK,0BAA0BF,OAAO,CAAC;IAC5E;IAEAD,KAAK,+BAA+B;QAChCP,OAAOK,qBAAqB,KAAKG,OAAO,CAAC;QACzCR,OAAOK,qBAAqB,MAAMG,OAAO,CAAC;QAC1CR,OAAOK,qBAAqB,OAAOG,OAAO,CAAC;QAC3CR,OAAOK,qBAAqB,SAASG,OAAO,CAAC;QAC7CR,OAAOK,qBAAqB,cAAcG,OAAO,CAAC;QAClDR,OAAOK,qBAAqB,gBAAgBG,OAAO,CAAC;QACpDR,OAAOK,qBAAqB,YAAYG,OAAO,CAAC;QAChDR,OAAOK,qBAAqB,YAAYG,OAAO,CAAC;IACpD;AACJ"}
|
package/dist/utils/test-utils.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { Container } from '@servicetitan/react-ioc';
|
|
2
|
-
export const initTestContainer = (serviceIdentifier, initDependenciesFn)
|
|
2
|
+
export const initTestContainer = (serviceIdentifier, initDependenciesFn)=>{
|
|
3
3
|
const rootContainer = new Container();
|
|
4
4
|
if (Array.isArray(serviceIdentifier)) {
|
|
5
|
-
serviceIdentifier.forEach(identifier
|
|
6
|
-
}
|
|
7
|
-
else {
|
|
5
|
+
serviceIdentifier.forEach((identifier)=>rootContainer.bind(identifier).toSelf());
|
|
6
|
+
} else {
|
|
8
7
|
rootContainer.bind(serviceIdentifier).toSelf();
|
|
9
8
|
}
|
|
10
|
-
return ()
|
|
9
|
+
return ()=>{
|
|
11
10
|
const container = new Container();
|
|
12
11
|
container.parent = rootContainer;
|
|
13
12
|
initDependenciesFn(container);
|
|
14
13
|
return container;
|
|
15
14
|
};
|
|
16
15
|
};
|
|
16
|
+
|
|
17
17
|
//# sourceMappingURL=test-utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"
|
|
1
|
+
{"version":3,"sources":["../../src/utils/test-utils.ts"],"sourcesContent":["import { Container } from '@servicetitan/react-ioc';\n\ntype Newable<T> = new (...args: never[]) => T;\n\nexport const initTestContainer = (\n serviceIdentifier: Newable<unknown> | Newable<unknown>[],\n initDependenciesFn: (container: Container) => void\n) => {\n const rootContainer = new Container();\n if (Array.isArray(serviceIdentifier)) {\n serviceIdentifier.forEach(identifier => rootContainer.bind(identifier).toSelf());\n } else {\n rootContainer.bind(serviceIdentifier).toSelf();\n }\n\n return () => {\n const container = new Container();\n container.parent = rootContainer;\n initDependenciesFn(container);\n return container;\n };\n};\n"],"names":["Container","initTestContainer","serviceIdentifier","initDependenciesFn","rootContainer","Array","isArray","forEach","identifier","bind","toSelf","container","parent"],"mappings":"AAAA,SAASA,SAAS,QAAQ,0BAA0B;AAIpD,OAAO,MAAMC,oBAAoB,CAC7BC,mBACAC;IAEA,MAAMC,gBAAgB,IAAIJ;IAC1B,IAAIK,MAAMC,OAAO,CAACJ,oBAAoB;QAClCA,kBAAkBK,OAAO,CAACC,CAAAA,aAAcJ,cAAcK,IAAI,CAACD,YAAYE,MAAM;IACjF,OAAO;QACHN,cAAcK,IAAI,CAACP,mBAAmBQ,MAAM;IAChD;IAEA,OAAO;QACH,MAAMC,YAAY,IAAIX;QACtBW,UAAUC,MAAM,GAAGR;QACnBD,mBAAmBQ;QACnB,OAAOA;IACX;AACJ,EAAE"}
|
package/dist/utils/text-utils.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
export function formatChatMessageDate(date) {
|
|
2
2
|
return date.toLocaleTimeString('en-US', {
|
|
3
3
|
hour: '2-digit',
|
|
4
|
-
minute: '2-digit'
|
|
4
|
+
minute: '2-digit'
|
|
5
5
|
});
|
|
6
6
|
}
|
|
7
|
-
export const getFirstName = (name)
|
|
7
|
+
export const getFirstName = (name)=>{
|
|
8
8
|
const parts = name.trim().split(' ');
|
|
9
9
|
return parts[0];
|
|
10
10
|
};
|
|
11
|
-
export const getNameInitials = (name)
|
|
11
|
+
export const getNameInitials = (name)=>{
|
|
12
12
|
const parts = name.trim().split(' ');
|
|
13
|
-
return parts
|
|
14
|
-
.filter(Boolean)
|
|
15
|
-
.splice(0, 2)
|
|
16
|
-
.map(p => p[0].toUpperCase())
|
|
17
|
-
.join('');
|
|
13
|
+
return parts.filter(Boolean).splice(0, 2).map((p)=>p[0].toUpperCase()).join('');
|
|
18
14
|
};
|
|
19
|
-
export const getNameInitialsFirst = (name)
|
|
15
|
+
export const getNameInitialsFirst = (name)=>{
|
|
20
16
|
const initials = getNameInitials(name);
|
|
21
17
|
return initials.length > 0 ? initials[0] : '';
|
|
22
18
|
};
|
|
23
|
-
export const extractFilenameAndExt = (fileName)
|
|
19
|
+
export const extractFilenameAndExt = (fileName)=>{
|
|
24
20
|
const lastIndex = fileName.lastIndexOf('.');
|
|
25
21
|
if (lastIndex === -1) {
|
|
26
|
-
return [
|
|
22
|
+
return [
|
|
23
|
+
fileName,
|
|
24
|
+
''
|
|
25
|
+
];
|
|
27
26
|
}
|
|
28
27
|
return [
|
|
29
28
|
fileName.substring(0, lastIndex) + '.',
|
|
30
|
-
fileName.substring(lastIndex + 1, fileName.length)
|
|
29
|
+
fileName.substring(lastIndex + 1, fileName.length)
|
|
31
30
|
];
|
|
32
31
|
};
|
|
32
|
+
|
|
33
33
|
//# sourceMappingURL=text-utils.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"
|
|
1
|
+
{"version":3,"sources":["../../src/utils/text-utils.ts"],"sourcesContent":["export function formatChatMessageDate(date: Date) {\n return date.toLocaleTimeString('en-US', {\n hour: '2-digit',\n minute: '2-digit',\n });\n}\n\nexport const getFirstName = (name: string): string => {\n const parts = name.trim().split(' ');\n return parts[0];\n};\n\nexport const getNameInitials = (name: string): string => {\n const parts = name.trim().split(' ');\n return parts\n .filter(Boolean)\n .splice(0, 2)\n .map(p => p[0]!.toUpperCase())\n .join('');\n};\n\nexport const getNameInitialsFirst = (name: string): string => {\n const initials = getNameInitials(name);\n return initials.length > 0 ? initials[0] : '';\n};\n\nexport const extractFilenameAndExt = (fileName: string): [string, string] => {\n const lastIndex = fileName.lastIndexOf('.');\n if (lastIndex === -1) {\n return [fileName, ''];\n }\n return [\n fileName.substring(0, lastIndex) + '.',\n fileName.substring(lastIndex + 1, fileName.length),\n ];\n};\n"],"names":["formatChatMessageDate","date","toLocaleTimeString","hour","minute","getFirstName","name","parts","trim","split","getNameInitials","filter","Boolean","splice","map","p","toUpperCase","join","getNameInitialsFirst","initials","length","extractFilenameAndExt","fileName","lastIndex","lastIndexOf","substring"],"mappings":"AAAA,OAAO,SAASA,sBAAsBC,IAAU;IAC5C,OAAOA,KAAKC,kBAAkB,CAAC,SAAS;QACpCC,MAAM;QACNC,QAAQ;IACZ;AACJ;AAEA,OAAO,MAAMC,eAAe,CAACC;IACzB,MAAMC,QAAQD,KAAKE,IAAI,GAAGC,KAAK,CAAC;IAChC,OAAOF,KAAK,CAAC,EAAE;AACnB,EAAE;AAEF,OAAO,MAAMG,kBAAkB,CAACJ;IAC5B,MAAMC,QAAQD,KAAKE,IAAI,GAAGC,KAAK,CAAC;IAChC,OAAOF,MACFI,MAAM,CAACC,SACPC,MAAM,CAAC,GAAG,GACVC,GAAG,CAACC,CAAAA,IAAKA,CAAC,CAAC,EAAE,CAAEC,WAAW,IAC1BC,IAAI,CAAC;AACd,EAAE;AAEF,OAAO,MAAMC,uBAAuB,CAACZ;IACjC,MAAMa,WAAWT,gBAAgBJ;IACjC,OAAOa,SAASC,MAAM,GAAG,IAAID,QAAQ,CAAC,EAAE,GAAG;AAC/C,EAAE;AAEF,OAAO,MAAME,wBAAwB,CAACC;IAClC,MAAMC,YAAYD,SAASE,WAAW,CAAC;IACvC,IAAID,cAAc,CAAC,GAAG;QAClB,OAAO;YAACD;YAAU;SAAG;IACzB;IACA,OAAO;QACHA,SAASG,SAAS,CAAC,GAAGF,aAAa;QACnCD,SAASG,SAAS,CAACF,YAAY,GAAGD,SAASF,MAAM;KACpD;AACL,EAAE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/titan-chat-ui-common",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Chat experience UI package common files",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"typings": "./dist/index.d.ts",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"push:local": "yalc push"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"@microsoft/fetch-event-source": "^2.0.1",
|
|
15
16
|
"lodash": "^4.18.1",
|
|
16
17
|
"nanoid": "^5.1.5"
|
|
17
18
|
},
|
|
@@ -32,5 +33,5 @@
|
|
|
32
33
|
"cli": {
|
|
33
34
|
"webpack": false
|
|
34
35
|
},
|
|
35
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "028073fe571014741da70bcff173d617e2738125"
|
|
36
37
|
}
|
package/src/index.ts
CHANGED
package/src/models/index.ts
CHANGED
|
@@ -531,9 +531,7 @@ export class ChatUiStore<T extends ChatCustomizations> implements IChatUiStore<T
|
|
|
531
531
|
return;
|
|
532
532
|
}
|
|
533
533
|
try {
|
|
534
|
-
|
|
535
|
-
this.incomingMessageSoundPromise = this.incomingMessageSound.play();
|
|
536
|
-
}
|
|
534
|
+
this.incomingMessageSoundPromise ??= this.incomingMessageSound.play();
|
|
537
535
|
await this.incomingMessageSoundPromise;
|
|
538
536
|
} finally {
|
|
539
537
|
this.incomingMessageSoundPromise = undefined;
|
package/src/stores/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
export type { IChatUiStore, ChatUiEventListener } from './chat-ui.store';
|
|
1
2
|
export {
|
|
2
3
|
CHAT_UI_STORE_TOKEN,
|
|
3
|
-
IChatUiStore,
|
|
4
4
|
ChatUiStore,
|
|
5
5
|
ChatUiEvent,
|
|
6
6
|
symbolUser,
|
|
7
7
|
symbolAgent,
|
|
8
|
-
ChatUiEventListener,
|
|
9
8
|
} from './chat-ui.store';
|
|
10
9
|
export { ChatUiBackendEchoStore } from './chat-ui-backend-echo.store';
|
|
11
|
-
export {
|
|
10
|
+
export type { IChatUiBackendStore } from './chat-ui-backend.store';
|
|
11
|
+
export { CHAT_UI_BACKEND_STORE_TOKEN } from './chat-ui-backend.store';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { type EventSourceMessage, fetchEventSource } from '@microsoft/fetch-event-source';
|
|
3
|
+
import { ChatSseClient } from '../chat-sse-client';
|
|
4
|
+
|
|
5
|
+
jest.mock('@microsoft/fetch-event-source');
|
|
6
|
+
|
|
7
|
+
const fetchEventSourceMock = fetchEventSource as jest.MockedFunction<typeof fetchEventSource>;
|
|
8
|
+
|
|
9
|
+
const msg = (event: string, data: string): EventSourceMessage => ({ id: '', event, data });
|
|
10
|
+
|
|
11
|
+
const okResponse = () =>
|
|
12
|
+
({ ok: true, status: 200, headers: { get: () => 'text/event-stream' } }) as unknown as Response;
|
|
13
|
+
const badResponse = () =>
|
|
14
|
+
({ ok: false, status: 502, headers: { get: () => 'text/html' } }) as unknown as Response;
|
|
15
|
+
|
|
16
|
+
describe('ChatSseClient — lifecycle wiring', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
fetchEventSourceMock.mockReset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('valid onopen reports connected', async () => {
|
|
22
|
+
const onConnected = jest.fn();
|
|
23
|
+
fetchEventSourceMock.mockImplementation(async (_url, init) => {
|
|
24
|
+
await init.onopen?.(okResponse());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
await new ChatSseClient({ url: 'u', onConnected }).start();
|
|
28
|
+
|
|
29
|
+
expect(onConnected).toHaveBeenCalledTimes(1);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('non-OK onopen is a connect-time failure: start rejects and onError fires', async () => {
|
|
33
|
+
const onError = jest.fn();
|
|
34
|
+
fetchEventSourceMock.mockImplementation(async (_url, init) => {
|
|
35
|
+
// Faithful to the lib: an onopen throw is routed to onerror, which (rethrowing) rejects.
|
|
36
|
+
try {
|
|
37
|
+
await init.onopen?.(badResponse());
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const result = init.onerror?.(err);
|
|
40
|
+
if (result === undefined || result === null) {
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const client = new ChatSseClient({ url: 'u', onError });
|
|
47
|
+
|
|
48
|
+
await expect(client.start()).rejects.toBeDefined();
|
|
49
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('malformed JSON data is skipped without tearing down or erroring', async () => {
|
|
53
|
+
const onError = jest.fn();
|
|
54
|
+
const onEvent = jest.fn();
|
|
55
|
+
let captured: any;
|
|
56
|
+
fetchEventSourceMock.mockImplementation(async (_url, init) => {
|
|
57
|
+
captured = init;
|
|
58
|
+
await init.onopen?.(okResponse());
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
await new ChatSseClient({ url: 'u', onError, onEvent }).start();
|
|
62
|
+
|
|
63
|
+
expect(() => captured.onmessage?.(msg('status.changed', 'not-json{'))).not.toThrow();
|
|
64
|
+
expect(onError).not.toHaveBeenCalled();
|
|
65
|
+
expect(onEvent).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { type EventSourceMessage, fetchEventSource } from '@microsoft/fetch-event-source';
|
|
3
|
+
import { ChatSseClient } from '../chat-sse-client';
|
|
4
|
+
|
|
5
|
+
jest.mock('@microsoft/fetch-event-source');
|
|
6
|
+
|
|
7
|
+
const fetchEventSourceMock = fetchEventSource as jest.MockedFunction<typeof fetchEventSource>;
|
|
8
|
+
|
|
9
|
+
const msg = (event: string, data: unknown): EventSourceMessage => ({
|
|
10
|
+
id: '',
|
|
11
|
+
event,
|
|
12
|
+
data: typeof data === 'string' ? data : JSON.stringify(data),
|
|
13
|
+
});
|
|
14
|
+
const okResponse = () =>
|
|
15
|
+
({ ok: true, status: 200, headers: { get: () => 'text/event-stream' } }) as unknown as Response;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Capture init, run a successful onopen, then stay open (pending) until the client aborts —
|
|
19
|
+
* mirroring real `fetchEventSource`, which resolves only when the stream actually closes.
|
|
20
|
+
*/
|
|
21
|
+
function captureOpen() {
|
|
22
|
+
const capture: { init?: any; opened: Promise<void> } = {
|
|
23
|
+
opened: undefined as any,
|
|
24
|
+
};
|
|
25
|
+
capture.opened = new Promise<void>(markOpened => {
|
|
26
|
+
fetchEventSourceMock.mockImplementation(async (_url, init) => {
|
|
27
|
+
capture.init = init;
|
|
28
|
+
await init.onopen?.(okResponse());
|
|
29
|
+
markOpened();
|
|
30
|
+
await new Promise<void>(resolve => {
|
|
31
|
+
const signal: AbortSignal | null | undefined = init.signal;
|
|
32
|
+
if (signal?.aborted) {
|
|
33
|
+
resolve();
|
|
34
|
+
} else {
|
|
35
|
+
signal?.addEventListener('abort', () => resolve(), { once: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
return capture;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('ChatSseClient — inactivity, reconnect, completion', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
fetchEventSourceMock.mockReset();
|
|
46
|
+
jest.useFakeTimers();
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
jest.useRealTimers();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('inactivity callback + onTimeout fire after the threshold, keeping the connection open', async () => {
|
|
53
|
+
const onInactivity = jest.fn();
|
|
54
|
+
const onTimeout = jest.fn();
|
|
55
|
+
const capture = captureOpen();
|
|
56
|
+
|
|
57
|
+
new ChatSseClient({
|
|
58
|
+
url: 'u',
|
|
59
|
+
inactivityTimeoutMs: 16_000,
|
|
60
|
+
onInactivity,
|
|
61
|
+
onTimeout,
|
|
62
|
+
}).start();
|
|
63
|
+
await capture.opened;
|
|
64
|
+
|
|
65
|
+
jest.advanceTimersByTime(15_999);
|
|
66
|
+
expect(onInactivity).not.toHaveBeenCalled();
|
|
67
|
+
|
|
68
|
+
jest.advanceTimersByTime(1);
|
|
69
|
+
expect(onInactivity).toHaveBeenCalledTimes(1);
|
|
70
|
+
expect(onTimeout).toHaveBeenCalledTimes(1);
|
|
71
|
+
// Connection is not aborted by inactivity.
|
|
72
|
+
expect(capture.init.signal?.aborted).toBeFalsy();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('a received event resets the inactivity timer', async () => {
|
|
76
|
+
const onInactivity = jest.fn();
|
|
77
|
+
const capture = captureOpen();
|
|
78
|
+
|
|
79
|
+
new ChatSseClient({ url: 'u', inactivityTimeoutMs: 16_000, onInactivity }).start();
|
|
80
|
+
await capture.opened;
|
|
81
|
+
|
|
82
|
+
jest.advanceTimersByTime(10_000);
|
|
83
|
+
capture.init.onmessage(msg('status.changed', { seq: 1 })); // reset
|
|
84
|
+
jest.advanceTimersByTime(10_000); // 10s since reset < 16s
|
|
85
|
+
expect(onInactivity).not.toHaveBeenCalled();
|
|
86
|
+
|
|
87
|
+
jest.advanceTimersByTime(6_000); // now 16s since reset
|
|
88
|
+
expect(onInactivity).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('transient drops reconnect up to the limit, then report a fatal error', async () => {
|
|
92
|
+
const onDisconnected = jest.fn();
|
|
93
|
+
const onError = jest.fn();
|
|
94
|
+
const capture = captureOpen();
|
|
95
|
+
|
|
96
|
+
new ChatSseClient({
|
|
97
|
+
url: 'u',
|
|
98
|
+
maxReconnectAttempts: 2,
|
|
99
|
+
onDisconnected,
|
|
100
|
+
onError,
|
|
101
|
+
}).start();
|
|
102
|
+
await capture.opened;
|
|
103
|
+
|
|
104
|
+
const r1 = capture.init.onerror(new Error('drop1'));
|
|
105
|
+
const r2 = capture.init.onerror(new Error('drop2'));
|
|
106
|
+
expect(typeof r1).toBe('number');
|
|
107
|
+
expect(typeof r2).toBe('number');
|
|
108
|
+
expect(onDisconnected).toHaveBeenCalledTimes(2);
|
|
109
|
+
expect(onError).not.toHaveBeenCalled();
|
|
110
|
+
|
|
111
|
+
expect(() => capture.init.onerror(new Error('drop3'))).toThrow();
|
|
112
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('a terminal event completes the run, fires onCompleted, and stops the timer', async () => {
|
|
116
|
+
const onCompleted = jest.fn();
|
|
117
|
+
const onInactivity = jest.fn();
|
|
118
|
+
const onFinished = jest.fn();
|
|
119
|
+
const capture = captureOpen();
|
|
120
|
+
|
|
121
|
+
new ChatSseClient({
|
|
122
|
+
url: 'u',
|
|
123
|
+
inactivityTimeoutMs: 16_000,
|
|
124
|
+
isTerminalEvent: e => e.event === 'run.finished',
|
|
125
|
+
handlers: { 'run.finished': onFinished },
|
|
126
|
+
onCompleted,
|
|
127
|
+
onInactivity,
|
|
128
|
+
}).start();
|
|
129
|
+
await capture.opened;
|
|
130
|
+
|
|
131
|
+
capture.init.onmessage(msg('run.finished', { seq: 1, answer: 'done' }));
|
|
132
|
+
|
|
133
|
+
expect(onFinished).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(onCompleted).toHaveBeenCalledTimes(1);
|
|
135
|
+
|
|
136
|
+
// After completion: timer stopped and post-terminal errors are not fatal.
|
|
137
|
+
jest.advanceTimersByTime(60_000);
|
|
138
|
+
expect(onInactivity).not.toHaveBeenCalled();
|
|
139
|
+
expect(() => capture.init.onerror(new Error('late'))).not.toThrow();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { type EventSourceMessage, fetchEventSource } from '@microsoft/fetch-event-source';
|
|
3
|
+
import { ChatSseClient } from '../chat-sse-client';
|
|
4
|
+
|
|
5
|
+
jest.mock('@microsoft/fetch-event-source');
|
|
6
|
+
|
|
7
|
+
const fetchEventSourceMock = fetchEventSource as jest.MockedFunction<typeof fetchEventSource>;
|
|
8
|
+
|
|
9
|
+
/** Capture the init passed to fetchEventSource so tests can drive its callbacks. */
|
|
10
|
+
function captureInit() {
|
|
11
|
+
const capture: { init?: Parameters<typeof fetchEventSource>[1]; url?: any } = {};
|
|
12
|
+
fetchEventSourceMock.mockImplementation((url, init) => {
|
|
13
|
+
capture.url = url;
|
|
14
|
+
capture.init = init;
|
|
15
|
+
return Promise.resolve();
|
|
16
|
+
});
|
|
17
|
+
return capture;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const msg = (event: string, data: unknown): EventSourceMessage => ({
|
|
21
|
+
id: '',
|
|
22
|
+
event,
|
|
23
|
+
data: typeof data === 'string' ? data : JSON.stringify(data),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('ChatSseClient — open / decode / dispatch / seq / registry', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
fetchEventSourceMock.mockReset();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('opens a POST stream with body, headers and event-stream Accept', async () => {
|
|
32
|
+
const capture = captureInit();
|
|
33
|
+
|
|
34
|
+
await new ChatSseClient({
|
|
35
|
+
url: 'https://api.test/api/v2/message/stream',
|
|
36
|
+
body: { question: 'hi', sessionId: 7 },
|
|
37
|
+
headers: { 'Authorization': 'Bearer t', 'X-Client-ID': 'help-center' },
|
|
38
|
+
}).start();
|
|
39
|
+
|
|
40
|
+
expect(capture.url).toBe('https://api.test/api/v2/message/stream');
|
|
41
|
+
expect(capture.init?.method).toBe('POST');
|
|
42
|
+
expect(capture.init?.body).toBe(JSON.stringify({ question: 'hi', sessionId: 7 }));
|
|
43
|
+
expect(capture.init?.headers).toMatchObject({
|
|
44
|
+
'Accept': 'text/event-stream',
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
'Authorization': 'Bearer t',
|
|
47
|
+
'X-Client-ID': 'help-center',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('JSON-decodes data and dispatches to the registered handler with seq', async () => {
|
|
52
|
+
const capture = captureInit();
|
|
53
|
+
const onStatus = jest.fn();
|
|
54
|
+
|
|
55
|
+
const client = new ChatSseClient({
|
|
56
|
+
url: 'u',
|
|
57
|
+
handlers: { 'status.changed': onStatus },
|
|
58
|
+
});
|
|
59
|
+
await client.start();
|
|
60
|
+
|
|
61
|
+
capture.init?.onmessage?.(msg('status.changed', { text: 'Thinking…', seq: 2 }));
|
|
62
|
+
|
|
63
|
+
expect(onStatus).toHaveBeenCalledTimes(1);
|
|
64
|
+
const [data, event] = onStatus.mock.calls[0] as any[];
|
|
65
|
+
expect(data).toEqual({ text: 'Thinking…', seq: 2 });
|
|
66
|
+
expect(event).toEqual({
|
|
67
|
+
event: 'status.changed',
|
|
68
|
+
data: { text: 'Thinking…', seq: 2 },
|
|
69
|
+
seq: 2,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('handlers registered via on() receive matching events; consumer can extend new types', async () => {
|
|
74
|
+
const capture = captureInit();
|
|
75
|
+
const onCustom = jest.fn();
|
|
76
|
+
|
|
77
|
+
const client = new ChatSseClient({ url: 'u' });
|
|
78
|
+
client.on('custom.future-event', onCustom);
|
|
79
|
+
await client.start();
|
|
80
|
+
|
|
81
|
+
capture.init?.onmessage?.(msg('custom.future-event', { foo: 1, seq: 1 }));
|
|
82
|
+
|
|
83
|
+
expect(onCustom).toHaveBeenCalledTimes(1);
|
|
84
|
+
expect((onCustom.mock.calls[0] as any[])[0]).toEqual({ foo: 1, seq: 1 });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('unknown event with no handler is ignored without closing; onEvent still fires', async () => {
|
|
88
|
+
const capture = captureInit();
|
|
89
|
+
const onEvent = jest.fn();
|
|
90
|
+
const onError = jest.fn();
|
|
91
|
+
|
|
92
|
+
const client = new ChatSseClient({ url: 'u', onEvent, onError });
|
|
93
|
+
await client.start();
|
|
94
|
+
|
|
95
|
+
expect(() => capture.init?.onmessage?.(msg('totally.unknown', { seq: 1 }))).not.toThrow();
|
|
96
|
+
|
|
97
|
+
expect(onError).not.toHaveBeenCalled();
|
|
98
|
+
expect(onEvent).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect((onEvent.mock.calls[0] as any[])[0]).toMatchObject({
|
|
100
|
+
event: 'totally.unknown',
|
|
101
|
+
seq: 1,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('ignores null / non-object JSON payloads without throwing or dispatching', async () => {
|
|
106
|
+
const capture = captureInit();
|
|
107
|
+
const onText = jest.fn();
|
|
108
|
+
const onEvent = jest.fn();
|
|
109
|
+
const onError = jest.fn();
|
|
110
|
+
|
|
111
|
+
const client = new ChatSseClient({
|
|
112
|
+
url: 'u',
|
|
113
|
+
handlers: { 'text.appended': onText },
|
|
114
|
+
onEvent,
|
|
115
|
+
onError,
|
|
116
|
+
});
|
|
117
|
+
await client.start();
|
|
118
|
+
|
|
119
|
+
// `JSON.parse` yields a value that has no `seq`; accessing `.seq` on `null` would throw.
|
|
120
|
+
expect(() => capture.init?.onmessage?.(msg('text.appended', null))).not.toThrow();
|
|
121
|
+
expect(() => capture.init?.onmessage?.(msg('text.appended', '42'))).not.toThrow();
|
|
122
|
+
expect(() => capture.init?.onmessage?.(msg('text.appended', '"hi"'))).not.toThrow();
|
|
123
|
+
// Unparseable data is likewise skipped.
|
|
124
|
+
expect(() => capture.init?.onmessage?.(msg('text.appended', 'not json'))).not.toThrow();
|
|
125
|
+
|
|
126
|
+
expect(onText).not.toHaveBeenCalled();
|
|
127
|
+
expect(onEvent).not.toHaveBeenCalled();
|
|
128
|
+
expect(onError).not.toHaveBeenCalled();
|
|
129
|
+
|
|
130
|
+
// A valid event after the bad ones is still processed (the stream is not wedged).
|
|
131
|
+
capture.init?.onmessage?.(msg('text.appended', { text: 'a', seq: 1 }));
|
|
132
|
+
expect(onText).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect((onText.mock.calls[0] as any[])[0]).toEqual({ text: 'a', seq: 1 });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('discards events whose seq is not greater than the last processed seq', async () => {
|
|
137
|
+
const capture = captureInit();
|
|
138
|
+
const onText = jest.fn();
|
|
139
|
+
|
|
140
|
+
const client = new ChatSseClient({ url: 'u', handlers: { 'text.appended': onText } });
|
|
141
|
+
await client.start();
|
|
142
|
+
|
|
143
|
+
capture.init?.onmessage?.(msg('text.appended', { text: 'a', seq: 5 }));
|
|
144
|
+
capture.init?.onmessage?.(msg('text.appended', { text: 'dup', seq: 5 })); // duplicate
|
|
145
|
+
capture.init?.onmessage?.(msg('text.appended', { text: 'old', seq: 3 })); // out of order
|
|
146
|
+
capture.init?.onmessage?.(msg('text.appended', { text: 'b', seq: 6 })); // newer
|
|
147
|
+
|
|
148
|
+
expect(onText).toHaveBeenCalledTimes(2);
|
|
149
|
+
expect((onText.mock.calls[0] as any[])[0]).toEqual({ text: 'a', seq: 5 });
|
|
150
|
+
expect((onText.mock.calls[1] as any[])[0]).toEqual({ text: 'b', seq: 6 });
|
|
151
|
+
});
|
|
152
|
+
});
|