@servicetitan/titan-chat-ui-common 8.0.0 → 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 +12 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/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/package.json +3 -2
- package/src/index.ts +1 -0
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# v9.0.0 (Mon Jun 08 2026)
|
|
2
|
+
|
|
3
|
+
#### 💥 Breaking Change
|
|
4
|
+
|
|
5
|
+
- SPA-8507: Agent Progress Streaming — Frontend Streaming UX [#91](https://github.com/servicetitan/titan-chatbot-client/pull/91) ([@AlexYarmolchuk](https://github.com/AlexYarmolchuk))
|
|
6
|
+
|
|
7
|
+
#### Authors: 1
|
|
8
|
+
|
|
9
|
+
- Alexandr Yarmolchuk ([@AlexYarmolchuk](https://github.com/AlexYarmolchuk))
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
1
13
|
# v8.0.0 (Wed Jun 03 2026)
|
|
2
14
|
|
|
3
15
|
#### 💥 Breaking Change
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC;AAC5B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './stores';\nexport * from './models';\nexport * from './hooks/use-customization-chat';\nexport * from './utils/text-utils';\nexport * from './utils/test-utils';\n"],"names":[],"mappings":"AAAA,cAAc,WAAW;AACzB,cAAc,WAAW;AACzB,cAAc,iCAAiC;AAC/C,cAAc,qBAAqB;AACnC,cAAc,qBAAqB"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './stores';\nexport * from './models';\nexport * from './streaming';\nexport * from './hooks/use-customization-chat';\nexport * from './utils/text-utils';\nexport * from './utils/test-utils';\n"],"names":[],"mappings":"AAAA,cAAc,WAAW;AACzB,cAAc,WAAW;AACzB,cAAc,cAAc;AAC5B,cAAc,iCAAiC;AAC/C,cAAc,qBAAqB;AACnC,cAAc,qBAAqB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-sse-client-lifecycle.test.d.ts","sourceRoot":"","sources":["../../../src/streaming/__tests__/chat-sse-client-lifecycle.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
3
|
+
import { ChatSseClient } from '../chat-sse-client';
|
|
4
|
+
jest.mock('@microsoft/fetch-event-source');
|
|
5
|
+
const fetchEventSourceMock = fetchEventSource;
|
|
6
|
+
const msg = (event, data)=>({
|
|
7
|
+
id: '',
|
|
8
|
+
event,
|
|
9
|
+
data
|
|
10
|
+
});
|
|
11
|
+
const okResponse = ()=>({
|
|
12
|
+
ok: true,
|
|
13
|
+
status: 200,
|
|
14
|
+
headers: {
|
|
15
|
+
get: ()=>'text/event-stream'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const badResponse = ()=>({
|
|
19
|
+
ok: false,
|
|
20
|
+
status: 502,
|
|
21
|
+
headers: {
|
|
22
|
+
get: ()=>'text/html'
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
describe('ChatSseClient — lifecycle wiring', ()=>{
|
|
26
|
+
beforeEach(()=>{
|
|
27
|
+
fetchEventSourceMock.mockReset();
|
|
28
|
+
});
|
|
29
|
+
test('valid onopen reports connected', async ()=>{
|
|
30
|
+
const onConnected = jest.fn();
|
|
31
|
+
fetchEventSourceMock.mockImplementation(async (_url, init)=>{
|
|
32
|
+
var _init_onopen;
|
|
33
|
+
await ((_init_onopen = init.onopen) === null || _init_onopen === void 0 ? void 0 : _init_onopen.call(init, okResponse()));
|
|
34
|
+
});
|
|
35
|
+
await new ChatSseClient({
|
|
36
|
+
url: 'u',
|
|
37
|
+
onConnected
|
|
38
|
+
}).start();
|
|
39
|
+
expect(onConnected).toHaveBeenCalledTimes(1);
|
|
40
|
+
});
|
|
41
|
+
test('non-OK onopen is a connect-time failure: start rejects and onError fires', async ()=>{
|
|
42
|
+
const onError = jest.fn();
|
|
43
|
+
fetchEventSourceMock.mockImplementation(async (_url, init)=>{
|
|
44
|
+
// Faithful to the lib: an onopen throw is routed to onerror, which (rethrowing) rejects.
|
|
45
|
+
try {
|
|
46
|
+
var _init_onopen;
|
|
47
|
+
await ((_init_onopen = init.onopen) === null || _init_onopen === void 0 ? void 0 : _init_onopen.call(init, badResponse()));
|
|
48
|
+
} catch (err) {
|
|
49
|
+
var _init_onerror;
|
|
50
|
+
const result = (_init_onerror = init.onerror) === null || _init_onerror === void 0 ? void 0 : _init_onerror.call(init, err);
|
|
51
|
+
if (result === undefined || result === null) {
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const client = new ChatSseClient({
|
|
57
|
+
url: 'u',
|
|
58
|
+
onError
|
|
59
|
+
});
|
|
60
|
+
await expect(client.start()).rejects.toBeDefined();
|
|
61
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
62
|
+
});
|
|
63
|
+
test('malformed JSON data is skipped without tearing down or erroring', async ()=>{
|
|
64
|
+
const onError = jest.fn();
|
|
65
|
+
const onEvent = jest.fn();
|
|
66
|
+
let captured;
|
|
67
|
+
fetchEventSourceMock.mockImplementation(async (_url, init)=>{
|
|
68
|
+
var _init_onopen;
|
|
69
|
+
captured = init;
|
|
70
|
+
await ((_init_onopen = init.onopen) === null || _init_onopen === void 0 ? void 0 : _init_onopen.call(init, okResponse()));
|
|
71
|
+
});
|
|
72
|
+
await new ChatSseClient({
|
|
73
|
+
url: 'u',
|
|
74
|
+
onError,
|
|
75
|
+
onEvent
|
|
76
|
+
}).start();
|
|
77
|
+
expect(()=>{
|
|
78
|
+
var _captured_onmessage;
|
|
79
|
+
return (_captured_onmessage = captured.onmessage) === null || _captured_onmessage === void 0 ? void 0 : _captured_onmessage.call(captured, msg('status.changed', 'not-json{'));
|
|
80
|
+
}).not.toThrow();
|
|
81
|
+
expect(onError).not.toHaveBeenCalled();
|
|
82
|
+
expect(onEvent).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
//# sourceMappingURL=chat-sse-client-lifecycle.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/streaming/__tests__/chat-sse-client-lifecycle.test.ts"],"sourcesContent":["import { beforeEach, describe, expect, jest, test } from '@jest/globals';\nimport { type EventSourceMessage, fetchEventSource } from '@microsoft/fetch-event-source';\nimport { ChatSseClient } from '../chat-sse-client';\n\njest.mock('@microsoft/fetch-event-source');\n\nconst fetchEventSourceMock = fetchEventSource as jest.MockedFunction<typeof fetchEventSource>;\n\nconst msg = (event: string, data: string): EventSourceMessage => ({ id: '', event, data });\n\nconst okResponse = () =>\n ({ ok: true, status: 200, headers: { get: () => 'text/event-stream' } }) as unknown as Response;\nconst badResponse = () =>\n ({ ok: false, status: 502, headers: { get: () => 'text/html' } }) as unknown as Response;\n\ndescribe('ChatSseClient — lifecycle wiring', () => {\n beforeEach(() => {\n fetchEventSourceMock.mockReset();\n });\n\n test('valid onopen reports connected', async () => {\n const onConnected = jest.fn();\n fetchEventSourceMock.mockImplementation(async (_url, init) => {\n await init.onopen?.(okResponse());\n });\n\n await new ChatSseClient({ url: 'u', onConnected }).start();\n\n expect(onConnected).toHaveBeenCalledTimes(1);\n });\n\n test('non-OK onopen is a connect-time failure: start rejects and onError fires', async () => {\n const onError = jest.fn();\n fetchEventSourceMock.mockImplementation(async (_url, init) => {\n // Faithful to the lib: an onopen throw is routed to onerror, which (rethrowing) rejects.\n try {\n await init.onopen?.(badResponse());\n } catch (err) {\n const result = init.onerror?.(err);\n if (result === undefined || result === null) {\n throw err;\n }\n }\n });\n\n const client = new ChatSseClient({ url: 'u', onError });\n\n await expect(client.start()).rejects.toBeDefined();\n expect(onError).toHaveBeenCalledTimes(1);\n });\n\n test('malformed JSON data is skipped without tearing down or erroring', async () => {\n const onError = jest.fn();\n const onEvent = jest.fn();\n let captured: any;\n fetchEventSourceMock.mockImplementation(async (_url, init) => {\n captured = init;\n await init.onopen?.(okResponse());\n });\n\n await new ChatSseClient({ url: 'u', onError, onEvent }).start();\n\n expect(() => captured.onmessage?.(msg('status.changed', 'not-json{'))).not.toThrow();\n expect(onError).not.toHaveBeenCalled();\n expect(onEvent).not.toHaveBeenCalled();\n });\n});\n"],"names":["beforeEach","describe","expect","jest","test","fetchEventSource","ChatSseClient","mock","fetchEventSourceMock","msg","event","data","id","okResponse","ok","status","headers","get","badResponse","mockReset","onConnected","fn","mockImplementation","_url","init","onopen","url","start","toHaveBeenCalledTimes","onError","err","result","onerror","undefined","client","rejects","toBeDefined","onEvent","captured","onmessage","not","toThrow","toHaveBeenCalled"],"mappings":"AAAA,SAASA,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,IAAI,EAAEC,IAAI,QAAQ,gBAAgB;AACzE,SAAkCC,gBAAgB,QAAQ,gCAAgC;AAC1F,SAASC,aAAa,QAAQ,qBAAqB;AAEnDH,KAAKI,IAAI,CAAC;AAEV,MAAMC,uBAAuBH;AAE7B,MAAMI,MAAM,CAACC,OAAeC,OAAsC,CAAA;QAAEC,IAAI;QAAIF;QAAOC;IAAK,CAAA;AAExF,MAAME,aAAa,IACd,CAAA;QAAEC,IAAI;QAAMC,QAAQ;QAAKC,SAAS;YAAEC,KAAK,IAAM;QAAoB;IAAE,CAAA;AAC1E,MAAMC,cAAc,IACf,CAAA;QAAEJ,IAAI;QAAOC,QAAQ;QAAKC,SAAS;YAAEC,KAAK,IAAM;QAAY;IAAE,CAAA;AAEnEhB,SAAS,oCAAoC;IACzCD,WAAW;QACPQ,qBAAqBW,SAAS;IAClC;IAEAf,KAAK,kCAAkC;QACnC,MAAMgB,cAAcjB,KAAKkB,EAAE;QAC3Bb,qBAAqBc,kBAAkB,CAAC,OAAOC,MAAMC;gBAC3CA;YAAN,QAAMA,eAAAA,KAAKC,MAAM,cAAXD,mCAAAA,kBAAAA,MAAcX;QACxB;QAEA,MAAM,IAAIP,cAAc;YAAEoB,KAAK;YAAKN;QAAY,GAAGO,KAAK;QAExDzB,OAAOkB,aAAaQ,qBAAqB,CAAC;IAC9C;IAEAxB,KAAK,4EAA4E;QAC7E,MAAMyB,UAAU1B,KAAKkB,EAAE;QACvBb,qBAAqBc,kBAAkB,CAAC,OAAOC,MAAMC;YACjD,yFAAyF;YACzF,IAAI;oBACMA;gBAAN,QAAMA,eAAAA,KAAKC,MAAM,cAAXD,mCAAAA,kBAAAA,MAAcN;YACxB,EAAE,OAAOY,KAAK;oBACKN;gBAAf,MAAMO,UAASP,gBAAAA,KAAKQ,OAAO,cAAZR,oCAAAA,mBAAAA,MAAeM;gBAC9B,IAAIC,WAAWE,aAAaF,WAAW,MAAM;oBACzC,MAAMD;gBACV;YACJ;QACJ;QAEA,MAAMI,SAAS,IAAI5B,cAAc;YAAEoB,KAAK;YAAKG;QAAQ;QAErD,MAAM3B,OAAOgC,OAAOP,KAAK,IAAIQ,OAAO,CAACC,WAAW;QAChDlC,OAAO2B,SAASD,qBAAqB,CAAC;IAC1C;IAEAxB,KAAK,mEAAmE;QACpE,MAAMyB,UAAU1B,KAAKkB,EAAE;QACvB,MAAMgB,UAAUlC,KAAKkB,EAAE;QACvB,IAAIiB;QACJ9B,qBAAqBc,kBAAkB,CAAC,OAAOC,MAAMC;gBAE3CA;YADNc,WAAWd;YACX,QAAMA,eAAAA,KAAKC,MAAM,cAAXD,mCAAAA,kBAAAA,MAAcX;QACxB;QAEA,MAAM,IAAIP,cAAc;YAAEoB,KAAK;YAAKG;YAASQ;QAAQ,GAAGV,KAAK;QAE7DzB,OAAO;gBAAMoC;oBAAAA,sBAAAA,SAASC,SAAS,cAAlBD,0CAAAA,yBAAAA,UAAqB7B,IAAI,kBAAkB;WAAe+B,GAAG,CAACC,OAAO;QAClFvC,OAAO2B,SAASW,GAAG,CAACE,gBAAgB;QACpCxC,OAAOmC,SAASG,GAAG,CAACE,gBAAgB;IACxC;AACJ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-sse-client-resilience.test.d.ts","sourceRoot":"","sources":["../../../src/streaming/__tests__/chat-sse-client-resilience.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
3
|
+
import { ChatSseClient } from '../chat-sse-client';
|
|
4
|
+
jest.mock('@microsoft/fetch-event-source');
|
|
5
|
+
const fetchEventSourceMock = fetchEventSource;
|
|
6
|
+
const msg = (event, data)=>({
|
|
7
|
+
id: '',
|
|
8
|
+
event,
|
|
9
|
+
data: typeof data === 'string' ? data : JSON.stringify(data)
|
|
10
|
+
});
|
|
11
|
+
const okResponse = ()=>({
|
|
12
|
+
ok: true,
|
|
13
|
+
status: 200,
|
|
14
|
+
headers: {
|
|
15
|
+
get: ()=>'text/event-stream'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
/**
|
|
19
|
+
* Capture init, run a successful onopen, then stay open (pending) until the client aborts —
|
|
20
|
+
* mirroring real `fetchEventSource`, which resolves only when the stream actually closes.
|
|
21
|
+
*/ function captureOpen() {
|
|
22
|
+
const capture = {
|
|
23
|
+
opened: undefined
|
|
24
|
+
};
|
|
25
|
+
capture.opened = new Promise((markOpened)=>{
|
|
26
|
+
fetchEventSourceMock.mockImplementation(async (_url, init)=>{
|
|
27
|
+
var _init_onopen;
|
|
28
|
+
capture.init = init;
|
|
29
|
+
await ((_init_onopen = init.onopen) === null || _init_onopen === void 0 ? void 0 : _init_onopen.call(init, okResponse()));
|
|
30
|
+
markOpened();
|
|
31
|
+
await new Promise((resolve)=>{
|
|
32
|
+
const signal = init.signal;
|
|
33
|
+
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
34
|
+
resolve();
|
|
35
|
+
} else {
|
|
36
|
+
signal === null || signal === void 0 ? void 0 : signal.addEventListener('abort', ()=>resolve(), {
|
|
37
|
+
once: true
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return capture;
|
|
44
|
+
}
|
|
45
|
+
describe('ChatSseClient — inactivity, reconnect, completion', ()=>{
|
|
46
|
+
beforeEach(()=>{
|
|
47
|
+
fetchEventSourceMock.mockReset();
|
|
48
|
+
jest.useFakeTimers();
|
|
49
|
+
});
|
|
50
|
+
afterEach(()=>{
|
|
51
|
+
jest.useRealTimers();
|
|
52
|
+
});
|
|
53
|
+
test('inactivity callback + onTimeout fire after the threshold, keeping the connection open', async ()=>{
|
|
54
|
+
var _capture_init_signal;
|
|
55
|
+
const onInactivity = jest.fn();
|
|
56
|
+
const onTimeout = jest.fn();
|
|
57
|
+
const capture = captureOpen();
|
|
58
|
+
new ChatSseClient({
|
|
59
|
+
url: 'u',
|
|
60
|
+
inactivityTimeoutMs: 16000,
|
|
61
|
+
onInactivity,
|
|
62
|
+
onTimeout
|
|
63
|
+
}).start();
|
|
64
|
+
await capture.opened;
|
|
65
|
+
jest.advanceTimersByTime(15999);
|
|
66
|
+
expect(onInactivity).not.toHaveBeenCalled();
|
|
67
|
+
jest.advanceTimersByTime(1);
|
|
68
|
+
expect(onInactivity).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(onTimeout).toHaveBeenCalledTimes(1);
|
|
70
|
+
// Connection is not aborted by inactivity.
|
|
71
|
+
expect((_capture_init_signal = capture.init.signal) === null || _capture_init_signal === void 0 ? void 0 : _capture_init_signal.aborted).toBeFalsy();
|
|
72
|
+
});
|
|
73
|
+
test('a received event resets the inactivity timer', async ()=>{
|
|
74
|
+
const onInactivity = jest.fn();
|
|
75
|
+
const capture = captureOpen();
|
|
76
|
+
new ChatSseClient({
|
|
77
|
+
url: 'u',
|
|
78
|
+
inactivityTimeoutMs: 16000,
|
|
79
|
+
onInactivity
|
|
80
|
+
}).start();
|
|
81
|
+
await capture.opened;
|
|
82
|
+
jest.advanceTimersByTime(10000);
|
|
83
|
+
capture.init.onmessage(msg('status.changed', {
|
|
84
|
+
seq: 1
|
|
85
|
+
})); // reset
|
|
86
|
+
jest.advanceTimersByTime(10000); // 10s since reset < 16s
|
|
87
|
+
expect(onInactivity).not.toHaveBeenCalled();
|
|
88
|
+
jest.advanceTimersByTime(6000); // now 16s since reset
|
|
89
|
+
expect(onInactivity).toHaveBeenCalledTimes(1);
|
|
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
|
+
new ChatSseClient({
|
|
96
|
+
url: 'u',
|
|
97
|
+
maxReconnectAttempts: 2,
|
|
98
|
+
onDisconnected,
|
|
99
|
+
onError
|
|
100
|
+
}).start();
|
|
101
|
+
await capture.opened;
|
|
102
|
+
const r1 = capture.init.onerror(new Error('drop1'));
|
|
103
|
+
const r2 = capture.init.onerror(new Error('drop2'));
|
|
104
|
+
expect(typeof r1).toBe('number');
|
|
105
|
+
expect(typeof r2).toBe('number');
|
|
106
|
+
expect(onDisconnected).toHaveBeenCalledTimes(2);
|
|
107
|
+
expect(onError).not.toHaveBeenCalled();
|
|
108
|
+
expect(()=>capture.init.onerror(new Error('drop3'))).toThrow();
|
|
109
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
110
|
+
});
|
|
111
|
+
test('a terminal event completes the run, fires onCompleted, and stops the timer', async ()=>{
|
|
112
|
+
const onCompleted = jest.fn();
|
|
113
|
+
const onInactivity = jest.fn();
|
|
114
|
+
const onFinished = jest.fn();
|
|
115
|
+
const capture = captureOpen();
|
|
116
|
+
new ChatSseClient({
|
|
117
|
+
url: 'u',
|
|
118
|
+
inactivityTimeoutMs: 16000,
|
|
119
|
+
isTerminalEvent: (e)=>e.event === 'run.finished',
|
|
120
|
+
handlers: {
|
|
121
|
+
'run.finished': onFinished
|
|
122
|
+
},
|
|
123
|
+
onCompleted,
|
|
124
|
+
onInactivity
|
|
125
|
+
}).start();
|
|
126
|
+
await capture.opened;
|
|
127
|
+
capture.init.onmessage(msg('run.finished', {
|
|
128
|
+
seq: 1,
|
|
129
|
+
answer: 'done'
|
|
130
|
+
}));
|
|
131
|
+
expect(onFinished).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(onCompleted).toHaveBeenCalledTimes(1);
|
|
133
|
+
// After completion: timer stopped and post-terminal errors are not fatal.
|
|
134
|
+
jest.advanceTimersByTime(60000);
|
|
135
|
+
expect(onInactivity).not.toHaveBeenCalled();
|
|
136
|
+
expect(()=>capture.init.onerror(new Error('late'))).not.toThrow();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
//# sourceMappingURL=chat-sse-client-resilience.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/streaming/__tests__/chat-sse-client-resilience.test.ts"],"sourcesContent":["import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals';\nimport { type EventSourceMessage, fetchEventSource } from '@microsoft/fetch-event-source';\nimport { ChatSseClient } from '../chat-sse-client';\n\njest.mock('@microsoft/fetch-event-source');\n\nconst fetchEventSourceMock = fetchEventSource as jest.MockedFunction<typeof fetchEventSource>;\n\nconst msg = (event: string, data: unknown): EventSourceMessage => ({\n id: '',\n event,\n data: typeof data === 'string' ? data : JSON.stringify(data),\n});\nconst okResponse = () =>\n ({ ok: true, status: 200, headers: { get: () => 'text/event-stream' } }) as unknown as Response;\n\n/**\n * Capture init, run a successful onopen, then stay open (pending) until the client aborts —\n * mirroring real `fetchEventSource`, which resolves only when the stream actually closes.\n */\nfunction captureOpen() {\n const capture: { init?: any; opened: Promise<void> } = {\n opened: undefined as any,\n };\n capture.opened = new Promise<void>(markOpened => {\n fetchEventSourceMock.mockImplementation(async (_url, init) => {\n capture.init = init;\n await init.onopen?.(okResponse());\n markOpened();\n await new Promise<void>(resolve => {\n const signal: AbortSignal | null | undefined = init.signal;\n if (signal?.aborted) {\n resolve();\n } else {\n signal?.addEventListener('abort', () => resolve(), { once: true });\n }\n });\n });\n });\n return capture;\n}\n\ndescribe('ChatSseClient — inactivity, reconnect, completion', () => {\n beforeEach(() => {\n fetchEventSourceMock.mockReset();\n jest.useFakeTimers();\n });\n afterEach(() => {\n jest.useRealTimers();\n });\n\n test('inactivity callback + onTimeout fire after the threshold, keeping the connection open', async () => {\n const onInactivity = jest.fn();\n const onTimeout = jest.fn();\n const capture = captureOpen();\n\n new ChatSseClient({\n url: 'u',\n inactivityTimeoutMs: 16_000,\n onInactivity,\n onTimeout,\n }).start();\n await capture.opened;\n\n jest.advanceTimersByTime(15_999);\n expect(onInactivity).not.toHaveBeenCalled();\n\n jest.advanceTimersByTime(1);\n expect(onInactivity).toHaveBeenCalledTimes(1);\n expect(onTimeout).toHaveBeenCalledTimes(1);\n // Connection is not aborted by inactivity.\n expect(capture.init.signal?.aborted).toBeFalsy();\n });\n\n test('a received event resets the inactivity timer', async () => {\n const onInactivity = jest.fn();\n const capture = captureOpen();\n\n new ChatSseClient({ url: 'u', inactivityTimeoutMs: 16_000, onInactivity }).start();\n await capture.opened;\n\n jest.advanceTimersByTime(10_000);\n capture.init.onmessage(msg('status.changed', { seq: 1 })); // reset\n jest.advanceTimersByTime(10_000); // 10s since reset < 16s\n expect(onInactivity).not.toHaveBeenCalled();\n\n jest.advanceTimersByTime(6_000); // now 16s since reset\n expect(onInactivity).toHaveBeenCalledTimes(1);\n });\n\n test('transient drops reconnect up to the limit, then report a fatal error', async () => {\n const onDisconnected = jest.fn();\n const onError = jest.fn();\n const capture = captureOpen();\n\n new ChatSseClient({\n url: 'u',\n maxReconnectAttempts: 2,\n onDisconnected,\n onError,\n }).start();\n await capture.opened;\n\n const r1 = capture.init.onerror(new Error('drop1'));\n const r2 = capture.init.onerror(new Error('drop2'));\n expect(typeof r1).toBe('number');\n expect(typeof r2).toBe('number');\n expect(onDisconnected).toHaveBeenCalledTimes(2);\n expect(onError).not.toHaveBeenCalled();\n\n expect(() => capture.init.onerror(new Error('drop3'))).toThrow();\n expect(onError).toHaveBeenCalledTimes(1);\n });\n\n test('a terminal event completes the run, fires onCompleted, and stops the timer', async () => {\n const onCompleted = jest.fn();\n const onInactivity = jest.fn();\n const onFinished = jest.fn();\n const capture = captureOpen();\n\n new ChatSseClient({\n url: 'u',\n inactivityTimeoutMs: 16_000,\n isTerminalEvent: e => e.event === 'run.finished',\n handlers: { 'run.finished': onFinished },\n onCompleted,\n onInactivity,\n }).start();\n await capture.opened;\n\n capture.init.onmessage(msg('run.finished', { seq: 1, answer: 'done' }));\n\n expect(onFinished).toHaveBeenCalledTimes(1);\n expect(onCompleted).toHaveBeenCalledTimes(1);\n\n // After completion: timer stopped and post-terminal errors are not fatal.\n jest.advanceTimersByTime(60_000);\n expect(onInactivity).not.toHaveBeenCalled();\n expect(() => capture.init.onerror(new Error('late'))).not.toThrow();\n });\n});\n"],"names":["afterEach","beforeEach","describe","expect","jest","test","fetchEventSource","ChatSseClient","mock","fetchEventSourceMock","msg","event","data","id","JSON","stringify","okResponse","ok","status","headers","get","captureOpen","capture","opened","undefined","Promise","markOpened","mockImplementation","_url","init","onopen","resolve","signal","aborted","addEventListener","once","mockReset","useFakeTimers","useRealTimers","onInactivity","fn","onTimeout","url","inactivityTimeoutMs","start","advanceTimersByTime","not","toHaveBeenCalled","toHaveBeenCalledTimes","toBeFalsy","onmessage","seq","onDisconnected","onError","maxReconnectAttempts","r1","onerror","Error","r2","toBe","toThrow","onCompleted","onFinished","isTerminalEvent","e","handlers","answer"],"mappings":"AAAA,SAASA,SAAS,EAAEC,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,IAAI,EAAEC,IAAI,QAAQ,gBAAgB;AACpF,SAAkCC,gBAAgB,QAAQ,gCAAgC;AAC1F,SAASC,aAAa,QAAQ,qBAAqB;AAEnDH,KAAKI,IAAI,CAAC;AAEV,MAAMC,uBAAuBH;AAE7B,MAAMI,MAAM,CAACC,OAAeC,OAAuC,CAAA;QAC/DC,IAAI;QACJF;QACAC,MAAM,OAAOA,SAAS,WAAWA,OAAOE,KAAKC,SAAS,CAACH;IAC3D,CAAA;AACA,MAAMI,aAAa,IACd,CAAA;QAAEC,IAAI;QAAMC,QAAQ;QAAKC,SAAS;YAAEC,KAAK,IAAM;QAAoB;IAAE,CAAA;AAE1E;;;CAGC,GACD,SAASC;IACL,MAAMC,UAAiD;QACnDC,QAAQC;IACZ;IACAF,QAAQC,MAAM,GAAG,IAAIE,QAAcC,CAAAA;QAC/BjB,qBAAqBkB,kBAAkB,CAAC,OAAOC,MAAMC;gBAE3CA;YADNP,QAAQO,IAAI,GAAGA;YACf,QAAMA,eAAAA,KAAKC,MAAM,cAAXD,mCAAAA,kBAAAA,MAAcb;YACpBU;YACA,MAAM,IAAID,QAAcM,CAAAA;gBACpB,MAAMC,SAAyCH,KAAKG,MAAM;gBAC1D,IAAIA,mBAAAA,6BAAAA,OAAQC,OAAO,EAAE;oBACjBF;gBACJ,OAAO;oBACHC,mBAAAA,6BAAAA,OAAQE,gBAAgB,CAAC,SAAS,IAAMH,WAAW;wBAAEI,MAAM;oBAAK;gBACpE;YACJ;QACJ;IACJ;IACA,OAAOb;AACX;AAEApB,SAAS,qDAAqD;IAC1DD,WAAW;QACPQ,qBAAqB2B,SAAS;QAC9BhC,KAAKiC,aAAa;IACtB;IACArC,UAAU;QACNI,KAAKkC,aAAa;IACtB;IAEAjC,KAAK,yFAAyF;YAoBnFiB;QAnBP,MAAMiB,eAAenC,KAAKoC,EAAE;QAC5B,MAAMC,YAAYrC,KAAKoC,EAAE;QACzB,MAAMlB,UAAUD;QAEhB,IAAId,cAAc;YACdmC,KAAK;YACLC,qBAAqB;YACrBJ;YACAE;QACJ,GAAGG,KAAK;QACR,MAAMtB,QAAQC,MAAM;QAEpBnB,KAAKyC,mBAAmB,CAAC;QACzB1C,OAAOoC,cAAcO,GAAG,CAACC,gBAAgB;QAEzC3C,KAAKyC,mBAAmB,CAAC;QACzB1C,OAAOoC,cAAcS,qBAAqB,CAAC;QAC3C7C,OAAOsC,WAAWO,qBAAqB,CAAC;QACxC,2CAA2C;QAC3C7C,QAAOmB,uBAAAA,QAAQO,IAAI,CAACG,MAAM,cAAnBV,2CAAAA,qBAAqBW,OAAO,EAAEgB,SAAS;IAClD;IAEA5C,KAAK,gDAAgD;QACjD,MAAMkC,eAAenC,KAAKoC,EAAE;QAC5B,MAAMlB,UAAUD;QAEhB,IAAId,cAAc;YAAEmC,KAAK;YAAKC,qBAAqB;YAAQJ;QAAa,GAAGK,KAAK;QAChF,MAAMtB,QAAQC,MAAM;QAEpBnB,KAAKyC,mBAAmB,CAAC;QACzBvB,QAAQO,IAAI,CAACqB,SAAS,CAACxC,IAAI,kBAAkB;YAAEyC,KAAK;QAAE,KAAK,QAAQ;QACnE/C,KAAKyC,mBAAmB,CAAC,QAAS,wBAAwB;QAC1D1C,OAAOoC,cAAcO,GAAG,CAACC,gBAAgB;QAEzC3C,KAAKyC,mBAAmB,CAAC,OAAQ,sBAAsB;QACvD1C,OAAOoC,cAAcS,qBAAqB,CAAC;IAC/C;IAEA3C,KAAK,wEAAwE;QACzE,MAAM+C,iBAAiBhD,KAAKoC,EAAE;QAC9B,MAAMa,UAAUjD,KAAKoC,EAAE;QACvB,MAAMlB,UAAUD;QAEhB,IAAId,cAAc;YACdmC,KAAK;YACLY,sBAAsB;YACtBF;YACAC;QACJ,GAAGT,KAAK;QACR,MAAMtB,QAAQC,MAAM;QAEpB,MAAMgC,KAAKjC,QAAQO,IAAI,CAAC2B,OAAO,CAAC,IAAIC,MAAM;QAC1C,MAAMC,KAAKpC,QAAQO,IAAI,CAAC2B,OAAO,CAAC,IAAIC,MAAM;QAC1CtD,OAAO,OAAOoD,IAAII,IAAI,CAAC;QACvBxD,OAAO,OAAOuD,IAAIC,IAAI,CAAC;QACvBxD,OAAOiD,gBAAgBJ,qBAAqB,CAAC;QAC7C7C,OAAOkD,SAASP,GAAG,CAACC,gBAAgB;QAEpC5C,OAAO,IAAMmB,QAAQO,IAAI,CAAC2B,OAAO,CAAC,IAAIC,MAAM,WAAWG,OAAO;QAC9DzD,OAAOkD,SAASL,qBAAqB,CAAC;IAC1C;IAEA3C,KAAK,8EAA8E;QAC/E,MAAMwD,cAAczD,KAAKoC,EAAE;QAC3B,MAAMD,eAAenC,KAAKoC,EAAE;QAC5B,MAAMsB,aAAa1D,KAAKoC,EAAE;QAC1B,MAAMlB,UAAUD;QAEhB,IAAId,cAAc;YACdmC,KAAK;YACLC,qBAAqB;YACrBoB,iBAAiBC,CAAAA,IAAKA,EAAErD,KAAK,KAAK;YAClCsD,UAAU;gBAAE,gBAAgBH;YAAW;YACvCD;YACAtB;QACJ,GAAGK,KAAK;QACR,MAAMtB,QAAQC,MAAM;QAEpBD,QAAQO,IAAI,CAACqB,SAAS,CAACxC,IAAI,gBAAgB;YAAEyC,KAAK;YAAGe,QAAQ;QAAO;QAEpE/D,OAAO2D,YAAYd,qBAAqB,CAAC;QACzC7C,OAAO0D,aAAab,qBAAqB,CAAC;QAE1C,0EAA0E;QAC1E5C,KAAKyC,mBAAmB,CAAC;QACzB1C,OAAOoC,cAAcO,GAAG,CAACC,gBAAgB;QACzC5C,OAAO,IAAMmB,QAAQO,IAAI,CAAC2B,OAAO,CAAC,IAAIC,MAAM,UAAUX,GAAG,CAACc,OAAO;IACrE;AACJ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat-sse-client.test.d.ts","sourceRoot":"","sources":["../../../src/streaming/__tests__/chat-sse-client.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
3
|
+
import { ChatSseClient } from '../chat-sse-client';
|
|
4
|
+
jest.mock('@microsoft/fetch-event-source');
|
|
5
|
+
const fetchEventSourceMock = fetchEventSource;
|
|
6
|
+
/** Capture the init passed to fetchEventSource so tests can drive its callbacks. */ function captureInit() {
|
|
7
|
+
const capture = {};
|
|
8
|
+
fetchEventSourceMock.mockImplementation((url, init)=>{
|
|
9
|
+
capture.url = url;
|
|
10
|
+
capture.init = init;
|
|
11
|
+
return Promise.resolve();
|
|
12
|
+
});
|
|
13
|
+
return capture;
|
|
14
|
+
}
|
|
15
|
+
const msg = (event, data)=>({
|
|
16
|
+
id: '',
|
|
17
|
+
event,
|
|
18
|
+
data: typeof data === 'string' ? data : JSON.stringify(data)
|
|
19
|
+
});
|
|
20
|
+
describe('ChatSseClient — open / decode / dispatch / seq / registry', ()=>{
|
|
21
|
+
beforeEach(()=>{
|
|
22
|
+
fetchEventSourceMock.mockReset();
|
|
23
|
+
});
|
|
24
|
+
test('opens a POST stream with body, headers and event-stream Accept', async ()=>{
|
|
25
|
+
var _capture_init, _capture_init1, _capture_init2;
|
|
26
|
+
const capture = captureInit();
|
|
27
|
+
await new ChatSseClient({
|
|
28
|
+
url: 'https://api.test/api/v2/message/stream',
|
|
29
|
+
body: {
|
|
30
|
+
question: 'hi',
|
|
31
|
+
sessionId: 7
|
|
32
|
+
},
|
|
33
|
+
headers: {
|
|
34
|
+
'Authorization': 'Bearer t',
|
|
35
|
+
'X-Client-ID': 'help-center'
|
|
36
|
+
}
|
|
37
|
+
}).start();
|
|
38
|
+
expect(capture.url).toBe('https://api.test/api/v2/message/stream');
|
|
39
|
+
expect((_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : _capture_init.method).toBe('POST');
|
|
40
|
+
expect((_capture_init1 = capture.init) === null || _capture_init1 === void 0 ? void 0 : _capture_init1.body).toBe(JSON.stringify({
|
|
41
|
+
question: 'hi',
|
|
42
|
+
sessionId: 7
|
|
43
|
+
}));
|
|
44
|
+
expect((_capture_init2 = capture.init) === null || _capture_init2 === void 0 ? void 0 : _capture_init2.headers).toMatchObject({
|
|
45
|
+
'Accept': 'text/event-stream',
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
'Authorization': 'Bearer t',
|
|
48
|
+
'X-Client-ID': 'help-center'
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
test('JSON-decodes data and dispatches to the registered handler with seq', async ()=>{
|
|
52
|
+
var _capture_init_onmessage, _capture_init;
|
|
53
|
+
const capture = captureInit();
|
|
54
|
+
const onStatus = jest.fn();
|
|
55
|
+
const client = new ChatSseClient({
|
|
56
|
+
url: 'u',
|
|
57
|
+
handlers: {
|
|
58
|
+
'status.changed': onStatus
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
await client.start();
|
|
62
|
+
(_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : (_capture_init_onmessage = _capture_init.onmessage) === null || _capture_init_onmessage === void 0 ? void 0 : _capture_init_onmessage.call(_capture_init, msg('status.changed', {
|
|
63
|
+
text: 'Thinking…',
|
|
64
|
+
seq: 2
|
|
65
|
+
}));
|
|
66
|
+
expect(onStatus).toHaveBeenCalledTimes(1);
|
|
67
|
+
const [data, event] = onStatus.mock.calls[0];
|
|
68
|
+
expect(data).toEqual({
|
|
69
|
+
text: 'Thinking…',
|
|
70
|
+
seq: 2
|
|
71
|
+
});
|
|
72
|
+
expect(event).toEqual({
|
|
73
|
+
event: 'status.changed',
|
|
74
|
+
data: {
|
|
75
|
+
text: 'Thinking…',
|
|
76
|
+
seq: 2
|
|
77
|
+
},
|
|
78
|
+
seq: 2
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
test('handlers registered via on() receive matching events; consumer can extend new types', async ()=>{
|
|
82
|
+
var _capture_init_onmessage, _capture_init;
|
|
83
|
+
const capture = captureInit();
|
|
84
|
+
const onCustom = jest.fn();
|
|
85
|
+
const client = new ChatSseClient({
|
|
86
|
+
url: 'u'
|
|
87
|
+
});
|
|
88
|
+
client.on('custom.future-event', onCustom);
|
|
89
|
+
await client.start();
|
|
90
|
+
(_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : (_capture_init_onmessage = _capture_init.onmessage) === null || _capture_init_onmessage === void 0 ? void 0 : _capture_init_onmessage.call(_capture_init, msg('custom.future-event', {
|
|
91
|
+
foo: 1,
|
|
92
|
+
seq: 1
|
|
93
|
+
}));
|
|
94
|
+
expect(onCustom).toHaveBeenCalledTimes(1);
|
|
95
|
+
expect(onCustom.mock.calls[0][0]).toEqual({
|
|
96
|
+
foo: 1,
|
|
97
|
+
seq: 1
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
test('unknown event with no handler is ignored without closing; onEvent still fires', async ()=>{
|
|
101
|
+
const capture = captureInit();
|
|
102
|
+
const onEvent = jest.fn();
|
|
103
|
+
const onError = jest.fn();
|
|
104
|
+
const client = new ChatSseClient({
|
|
105
|
+
url: 'u',
|
|
106
|
+
onEvent,
|
|
107
|
+
onError
|
|
108
|
+
});
|
|
109
|
+
await client.start();
|
|
110
|
+
expect(()=>{
|
|
111
|
+
var _capture_init_onmessage, _capture_init;
|
|
112
|
+
return (_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : (_capture_init_onmessage = _capture_init.onmessage) === null || _capture_init_onmessage === void 0 ? void 0 : _capture_init_onmessage.call(_capture_init, msg('totally.unknown', {
|
|
113
|
+
seq: 1
|
|
114
|
+
}));
|
|
115
|
+
}).not.toThrow();
|
|
116
|
+
expect(onError).not.toHaveBeenCalled();
|
|
117
|
+
expect(onEvent).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(onEvent.mock.calls[0][0]).toMatchObject({
|
|
119
|
+
event: 'totally.unknown',
|
|
120
|
+
seq: 1
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
test('ignores null / non-object JSON payloads without throwing or dispatching', async ()=>{
|
|
124
|
+
var // A valid event after the bad ones is still processed (the stream is not wedged).
|
|
125
|
+
_capture_init_onmessage, _capture_init;
|
|
126
|
+
const capture = captureInit();
|
|
127
|
+
const onText = jest.fn();
|
|
128
|
+
const onEvent = jest.fn();
|
|
129
|
+
const onError = jest.fn();
|
|
130
|
+
const client = new ChatSseClient({
|
|
131
|
+
url: 'u',
|
|
132
|
+
handlers: {
|
|
133
|
+
'text.appended': onText
|
|
134
|
+
},
|
|
135
|
+
onEvent,
|
|
136
|
+
onError
|
|
137
|
+
});
|
|
138
|
+
await client.start();
|
|
139
|
+
// `JSON.parse` yields a value that has no `seq`; accessing `.seq` on `null` would throw.
|
|
140
|
+
expect(()=>{
|
|
141
|
+
var _capture_init_onmessage, _capture_init;
|
|
142
|
+
return (_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : (_capture_init_onmessage = _capture_init.onmessage) === null || _capture_init_onmessage === void 0 ? void 0 : _capture_init_onmessage.call(_capture_init, msg('text.appended', null));
|
|
143
|
+
}).not.toThrow();
|
|
144
|
+
expect(()=>{
|
|
145
|
+
var _capture_init_onmessage, _capture_init;
|
|
146
|
+
return (_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : (_capture_init_onmessage = _capture_init.onmessage) === null || _capture_init_onmessage === void 0 ? void 0 : _capture_init_onmessage.call(_capture_init, msg('text.appended', '42'));
|
|
147
|
+
}).not.toThrow();
|
|
148
|
+
expect(()=>{
|
|
149
|
+
var _capture_init_onmessage, _capture_init;
|
|
150
|
+
return (_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : (_capture_init_onmessage = _capture_init.onmessage) === null || _capture_init_onmessage === void 0 ? void 0 : _capture_init_onmessage.call(_capture_init, msg('text.appended', '"hi"'));
|
|
151
|
+
}).not.toThrow();
|
|
152
|
+
// Unparseable data is likewise skipped.
|
|
153
|
+
expect(()=>{
|
|
154
|
+
var _capture_init_onmessage, _capture_init;
|
|
155
|
+
return (_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : (_capture_init_onmessage = _capture_init.onmessage) === null || _capture_init_onmessage === void 0 ? void 0 : _capture_init_onmessage.call(_capture_init, msg('text.appended', 'not json'));
|
|
156
|
+
}).not.toThrow();
|
|
157
|
+
expect(onText).not.toHaveBeenCalled();
|
|
158
|
+
expect(onEvent).not.toHaveBeenCalled();
|
|
159
|
+
expect(onError).not.toHaveBeenCalled();
|
|
160
|
+
(_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : (_capture_init_onmessage = _capture_init.onmessage) === null || _capture_init_onmessage === void 0 ? void 0 : _capture_init_onmessage.call(_capture_init, msg('text.appended', {
|
|
161
|
+
text: 'a',
|
|
162
|
+
seq: 1
|
|
163
|
+
}));
|
|
164
|
+
expect(onText).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(onText.mock.calls[0][0]).toEqual({
|
|
166
|
+
text: 'a',
|
|
167
|
+
seq: 1
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
test('discards events whose seq is not greater than the last processed seq', async ()=>{
|
|
171
|
+
var _capture_init_onmessage, _capture_init, _capture_init_onmessage1, _capture_init1, _capture_init_onmessage2, _capture_init2, _capture_init_onmessage3, _capture_init3;
|
|
172
|
+
const capture = captureInit();
|
|
173
|
+
const onText = jest.fn();
|
|
174
|
+
const client = new ChatSseClient({
|
|
175
|
+
url: 'u',
|
|
176
|
+
handlers: {
|
|
177
|
+
'text.appended': onText
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
await client.start();
|
|
181
|
+
(_capture_init = capture.init) === null || _capture_init === void 0 ? void 0 : (_capture_init_onmessage = _capture_init.onmessage) === null || _capture_init_onmessage === void 0 ? void 0 : _capture_init_onmessage.call(_capture_init, msg('text.appended', {
|
|
182
|
+
text: 'a',
|
|
183
|
+
seq: 5
|
|
184
|
+
}));
|
|
185
|
+
(_capture_init1 = capture.init) === null || _capture_init1 === void 0 ? void 0 : (_capture_init_onmessage1 = _capture_init1.onmessage) === null || _capture_init_onmessage1 === void 0 ? void 0 : _capture_init_onmessage1.call(_capture_init1, msg('text.appended', {
|
|
186
|
+
text: 'dup',
|
|
187
|
+
seq: 5
|
|
188
|
+
})); // duplicate
|
|
189
|
+
(_capture_init2 = capture.init) === null || _capture_init2 === void 0 ? void 0 : (_capture_init_onmessage2 = _capture_init2.onmessage) === null || _capture_init_onmessage2 === void 0 ? void 0 : _capture_init_onmessage2.call(_capture_init2, msg('text.appended', {
|
|
190
|
+
text: 'old',
|
|
191
|
+
seq: 3
|
|
192
|
+
})); // out of order
|
|
193
|
+
(_capture_init3 = capture.init) === null || _capture_init3 === void 0 ? void 0 : (_capture_init_onmessage3 = _capture_init3.onmessage) === null || _capture_init_onmessage3 === void 0 ? void 0 : _capture_init_onmessage3.call(_capture_init3, msg('text.appended', {
|
|
194
|
+
text: 'b',
|
|
195
|
+
seq: 6
|
|
196
|
+
})); // newer
|
|
197
|
+
expect(onText).toHaveBeenCalledTimes(2);
|
|
198
|
+
expect(onText.mock.calls[0][0]).toEqual({
|
|
199
|
+
text: 'a',
|
|
200
|
+
seq: 5
|
|
201
|
+
});
|
|
202
|
+
expect(onText.mock.calls[1][0]).toEqual({
|
|
203
|
+
text: 'b',
|
|
204
|
+
seq: 6
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
//# sourceMappingURL=chat-sse-client.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/streaming/__tests__/chat-sse-client.test.ts"],"sourcesContent":["import { beforeEach, describe, expect, jest, test } from '@jest/globals';\nimport { type EventSourceMessage, fetchEventSource } from '@microsoft/fetch-event-source';\nimport { ChatSseClient } from '../chat-sse-client';\n\njest.mock('@microsoft/fetch-event-source');\n\nconst fetchEventSourceMock = fetchEventSource as jest.MockedFunction<typeof fetchEventSource>;\n\n/** Capture the init passed to fetchEventSource so tests can drive its callbacks. */\nfunction captureInit() {\n const capture: { init?: Parameters<typeof fetchEventSource>[1]; url?: any } = {};\n fetchEventSourceMock.mockImplementation((url, init) => {\n capture.url = url;\n capture.init = init;\n return Promise.resolve();\n });\n return capture;\n}\n\nconst msg = (event: string, data: unknown): EventSourceMessage => ({\n id: '',\n event,\n data: typeof data === 'string' ? data : JSON.stringify(data),\n});\n\ndescribe('ChatSseClient — open / decode / dispatch / seq / registry', () => {\n beforeEach(() => {\n fetchEventSourceMock.mockReset();\n });\n\n test('opens a POST stream with body, headers and event-stream Accept', async () => {\n const capture = captureInit();\n\n await new ChatSseClient({\n url: 'https://api.test/api/v2/message/stream',\n body: { question: 'hi', sessionId: 7 },\n headers: { 'Authorization': 'Bearer t', 'X-Client-ID': 'help-center' },\n }).start();\n\n expect(capture.url).toBe('https://api.test/api/v2/message/stream');\n expect(capture.init?.method).toBe('POST');\n expect(capture.init?.body).toBe(JSON.stringify({ question: 'hi', sessionId: 7 }));\n expect(capture.init?.headers).toMatchObject({\n 'Accept': 'text/event-stream',\n 'Content-Type': 'application/json',\n 'Authorization': 'Bearer t',\n 'X-Client-ID': 'help-center',\n });\n });\n\n test('JSON-decodes data and dispatches to the registered handler with seq', async () => {\n const capture = captureInit();\n const onStatus = jest.fn();\n\n const client = new ChatSseClient({\n url: 'u',\n handlers: { 'status.changed': onStatus },\n });\n await client.start();\n\n capture.init?.onmessage?.(msg('status.changed', { text: 'Thinking…', seq: 2 }));\n\n expect(onStatus).toHaveBeenCalledTimes(1);\n const [data, event] = onStatus.mock.calls[0] as any[];\n expect(data).toEqual({ text: 'Thinking…', seq: 2 });\n expect(event).toEqual({\n event: 'status.changed',\n data: { text: 'Thinking…', seq: 2 },\n seq: 2,\n });\n });\n\n test('handlers registered via on() receive matching events; consumer can extend new types', async () => {\n const capture = captureInit();\n const onCustom = jest.fn();\n\n const client = new ChatSseClient({ url: 'u' });\n client.on('custom.future-event', onCustom);\n await client.start();\n\n capture.init?.onmessage?.(msg('custom.future-event', { foo: 1, seq: 1 }));\n\n expect(onCustom).toHaveBeenCalledTimes(1);\n expect((onCustom.mock.calls[0] as any[])[0]).toEqual({ foo: 1, seq: 1 });\n });\n\n test('unknown event with no handler is ignored without closing; onEvent still fires', async () => {\n const capture = captureInit();\n const onEvent = jest.fn();\n const onError = jest.fn();\n\n const client = new ChatSseClient({ url: 'u', onEvent, onError });\n await client.start();\n\n expect(() => capture.init?.onmessage?.(msg('totally.unknown', { seq: 1 }))).not.toThrow();\n\n expect(onError).not.toHaveBeenCalled();\n expect(onEvent).toHaveBeenCalledTimes(1);\n expect((onEvent.mock.calls[0] as any[])[0]).toMatchObject({\n event: 'totally.unknown',\n seq: 1,\n });\n });\n\n test('ignores null / non-object JSON payloads without throwing or dispatching', async () => {\n const capture = captureInit();\n const onText = jest.fn();\n const onEvent = jest.fn();\n const onError = jest.fn();\n\n const client = new ChatSseClient({\n url: 'u',\n handlers: { 'text.appended': onText },\n onEvent,\n onError,\n });\n await client.start();\n\n // `JSON.parse` yields a value that has no `seq`; accessing `.seq` on `null` would throw.\n expect(() => capture.init?.onmessage?.(msg('text.appended', null))).not.toThrow();\n expect(() => capture.init?.onmessage?.(msg('text.appended', '42'))).not.toThrow();\n expect(() => capture.init?.onmessage?.(msg('text.appended', '\"hi\"'))).not.toThrow();\n // Unparseable data is likewise skipped.\n expect(() => capture.init?.onmessage?.(msg('text.appended', 'not json'))).not.toThrow();\n\n expect(onText).not.toHaveBeenCalled();\n expect(onEvent).not.toHaveBeenCalled();\n expect(onError).not.toHaveBeenCalled();\n\n // A valid event after the bad ones is still processed (the stream is not wedged).\n capture.init?.onmessage?.(msg('text.appended', { text: 'a', seq: 1 }));\n expect(onText).toHaveBeenCalledTimes(1);\n expect((onText.mock.calls[0] as any[])[0]).toEqual({ text: 'a', seq: 1 });\n });\n\n test('discards events whose seq is not greater than the last processed seq', async () => {\n const capture = captureInit();\n const onText = jest.fn();\n\n const client = new ChatSseClient({ url: 'u', handlers: { 'text.appended': onText } });\n await client.start();\n\n capture.init?.onmessage?.(msg('text.appended', { text: 'a', seq: 5 }));\n capture.init?.onmessage?.(msg('text.appended', { text: 'dup', seq: 5 })); // duplicate\n capture.init?.onmessage?.(msg('text.appended', { text: 'old', seq: 3 })); // out of order\n capture.init?.onmessage?.(msg('text.appended', { text: 'b', seq: 6 })); // newer\n\n expect(onText).toHaveBeenCalledTimes(2);\n expect((onText.mock.calls[0] as any[])[0]).toEqual({ text: 'a', seq: 5 });\n expect((onText.mock.calls[1] as any[])[0]).toEqual({ text: 'b', seq: 6 });\n });\n});\n"],"names":["beforeEach","describe","expect","jest","test","fetchEventSource","ChatSseClient","mock","fetchEventSourceMock","captureInit","capture","mockImplementation","url","init","Promise","resolve","msg","event","data","id","JSON","stringify","mockReset","body","question","sessionId","headers","start","toBe","method","toMatchObject","onStatus","fn","client","handlers","onmessage","text","seq","toHaveBeenCalledTimes","calls","toEqual","onCustom","on","foo","onEvent","onError","not","toThrow","toHaveBeenCalled","onText"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-wiring.test.d.ts","sourceRoot":"","sources":["../../../src/streaming/__tests__/mock-wiring.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
3
|
+
jest.mock('@microsoft/fetch-event-source');
|
|
4
|
+
const fetchEventSourceMock = fetchEventSource;
|
|
5
|
+
/**
|
|
6
|
+
* Task 1.2 — proves the test harness can mock `@microsoft/fetch-event-source`
|
|
7
|
+
* before the real streaming-client work begins. This is intentionally trivial.
|
|
8
|
+
*/ describe('fetch-event-source mock wiring', ()=>{
|
|
9
|
+
beforeEach(()=>{
|
|
10
|
+
fetchEventSourceMock.mockReset();
|
|
11
|
+
});
|
|
12
|
+
test('fetchEventSource is mockable and observable', async ()=>{
|
|
13
|
+
fetchEventSourceMock.mockResolvedValue(undefined);
|
|
14
|
+
await fetchEventSource('https://example.test/stream', {});
|
|
15
|
+
expect(fetchEventSourceMock).toHaveBeenCalledTimes(1);
|
|
16
|
+
expect(fetchEventSourceMock).toHaveBeenCalledWith('https://example.test/stream', {});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=mock-wiring.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/streaming/__tests__/mock-wiring.test.ts"],"sourcesContent":["import { beforeEach, describe, expect, jest, test } from '@jest/globals';\nimport { fetchEventSource } from '@microsoft/fetch-event-source';\n\njest.mock('@microsoft/fetch-event-source');\n\nconst fetchEventSourceMock = fetchEventSource as jest.MockedFunction<typeof fetchEventSource>;\n\n/**\n * Task 1.2 — proves the test harness can mock `@microsoft/fetch-event-source`\n * before the real streaming-client work begins. This is intentionally trivial.\n */\ndescribe('fetch-event-source mock wiring', () => {\n beforeEach(() => {\n fetchEventSourceMock.mockReset();\n });\n\n test('fetchEventSource is mockable and observable', async () => {\n fetchEventSourceMock.mockResolvedValue(undefined);\n\n await fetchEventSource('https://example.test/stream', {});\n\n expect(fetchEventSourceMock).toHaveBeenCalledTimes(1);\n expect(fetchEventSourceMock).toHaveBeenCalledWith('https://example.test/stream', {});\n });\n});\n"],"names":["beforeEach","describe","expect","jest","test","fetchEventSource","mock","fetchEventSourceMock","mockReset","mockResolvedValue","undefined","toHaveBeenCalledTimes","toHaveBeenCalledWith"],"mappings":"AAAA,SAASA,UAAU,EAAEC,QAAQ,EAAEC,MAAM,EAAEC,IAAI,EAAEC,IAAI,QAAQ,gBAAgB;AACzE,SAASC,gBAAgB,QAAQ,gCAAgC;AAEjEF,KAAKG,IAAI,CAAC;AAEV,MAAMC,uBAAuBF;AAE7B;;;CAGC,GACDJ,SAAS,kCAAkC;IACvCD,WAAW;QACPO,qBAAqBC,SAAS;IAClC;IAEAJ,KAAK,+CAA+C;QAChDG,qBAAqBE,iBAAiB,CAACC;QAEvC,MAAML,iBAAiB,+BAA+B,CAAC;QAEvDH,OAAOK,sBAAsBI,qBAAqB,CAAC;QACnDT,OAAOK,sBAAsBK,oBAAoB,CAAC,+BAA+B,CAAC;IACtF;AACJ"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"streaming-progress.model.test.d.ts","sourceRoot":"","sources":["../../../src/streaming/__tests__/streaming-progress.model.test.ts"],"names":[],"mappings":""}
|