@equinor/fusion-framework-module-http 6.2.5 → 6.3.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 +22 -0
- package/README.md +182 -17
- package/dist/esm/errors.js +22 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/lib/client/client.js +35 -0
- package/dist/esm/lib/client/client.js.map +1 -1
- package/dist/esm/lib/operators/sse.operator.js +33 -0
- package/dist/esm/lib/operators/sse.operator.js.map +1 -0
- package/dist/esm/lib/selectors/sse-selector.js +164 -0
- package/dist/esm/lib/selectors/sse-selector.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/errors.d.ts +21 -0
- package/dist/types/lib/client/client.d.ts +24 -0
- package/dist/types/lib/operators/fetch-request.schema.d.ts +4 -4
- package/dist/types/lib/operators/sse.operator.d.ts +32 -0
- package/dist/types/lib/selectors/sse-selector.d.ts +63 -0
- package/dist/types/version.d.ts +1 -1
- package/package.json +3 -3
- package/src/errors.ts +27 -0
- package/src/lib/client/client.ts +52 -0
- package/src/lib/operators/sse.operator.ts +44 -0
- package/src/lib/selectors/sse-selector.ts +262 -0
- package/src/version.ts +1 -1
- package/tests/sse.selector.test.ts +150 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ServerSentEventResponseError } from '../src/errors.js';
|
|
3
|
+
import { createSseSelector } from '../src/lib/selectors/sse-selector.js'; // Adjust the import path as needed
|
|
4
|
+
import { of, lastValueFrom } from 'rxjs';
|
|
5
|
+
import { concatMap, scan } from 'rxjs/operators';
|
|
6
|
+
import { set } from 'zod';
|
|
7
|
+
|
|
8
|
+
// Helper to create a mock Response
|
|
9
|
+
function createMockResponse(
|
|
10
|
+
bodyChunks: (Uint8Array | null)[],
|
|
11
|
+
headers: Record<string, string> = { 'Content-Type': 'text/event-stream' },
|
|
12
|
+
status = 200,
|
|
13
|
+
): Response {
|
|
14
|
+
const body = new ReadableStream({
|
|
15
|
+
start(controller) {
|
|
16
|
+
for (const chunk of bodyChunks) {
|
|
17
|
+
if (chunk === null) {
|
|
18
|
+
controller.close();
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
controller.enqueue(chunk);
|
|
22
|
+
}
|
|
23
|
+
controller.close();
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
return new Response(body, { status, headers });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('createSseSelector', () => {
|
|
30
|
+
it('emits parsed events from a valid SSE response', async () => {
|
|
31
|
+
const chunks = [new Uint8Array(new TextEncoder().encode('data: {"key": "value"}\n\n'))];
|
|
32
|
+
const response = createMockResponse(chunks);
|
|
33
|
+
const selector = createSseSelector();
|
|
34
|
+
const events = await lastValueFrom(
|
|
35
|
+
of(response).pipe(
|
|
36
|
+
concatMap(selector),
|
|
37
|
+
scan((acc, event) => [...acc, event], [] as any[]),
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
expect(events).toEqual([{ data: { key: 'value' } }]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('throws on non-200 response', () => {
|
|
44
|
+
const response = createMockResponse([], {}, 500);
|
|
45
|
+
const selector = createSseSelector();
|
|
46
|
+
|
|
47
|
+
expect(() => selector(response)).toThrowError(
|
|
48
|
+
new ServerSentEventResponseError('HTTP error! Status: 500', response),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('throws on missing body', () => {
|
|
53
|
+
const response = new Response(null, {
|
|
54
|
+
headers: { 'Content-Type': 'text/event-stream' },
|
|
55
|
+
});
|
|
56
|
+
const selector = createSseSelector();
|
|
57
|
+
|
|
58
|
+
expect(() => selector(response)).toThrowError(
|
|
59
|
+
new ServerSentEventResponseError('Response body is not readable', response),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('throws on invalid Content-Type', () => {
|
|
64
|
+
const response = createMockResponse([], { 'Content-Type': 'application/json' });
|
|
65
|
+
const selector = createSseSelector();
|
|
66
|
+
|
|
67
|
+
expect(() => selector(response)).toThrowError(
|
|
68
|
+
new ServerSentEventResponseError('Response is not a text/event-stream', response),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('filters events by event type', async () => {
|
|
73
|
+
const chunks = [
|
|
74
|
+
new Uint8Array(new TextEncoder().encode('event: message\ndata: {"key": "value"}\n\n')),
|
|
75
|
+
new Uint8Array(new TextEncoder().encode('event: other\ndata: {"other": "data"}\n\n')),
|
|
76
|
+
];
|
|
77
|
+
const response = createMockResponse(chunks);
|
|
78
|
+
const selector = createSseSelector({ eventFilter: 'message' });
|
|
79
|
+
const events = await lastValueFrom(
|
|
80
|
+
of(response).pipe(
|
|
81
|
+
concatMap(selector),
|
|
82
|
+
scan((acc, event) => [...acc, event], [] as any[]),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(events).toEqual([{ event: 'message', data: { key: 'value' } }]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('skips heartbeat events when skipHeartbeats is true', async () => {
|
|
90
|
+
const chunks = [
|
|
91
|
+
new Uint8Array(new TextEncoder().encode(':heartbeat\n\n')), // Heartbeat
|
|
92
|
+
new Uint8Array(new TextEncoder().encode('event: heartbeat\n\n')), // Heartbeat
|
|
93
|
+
new Uint8Array(new TextEncoder().encode('event: ping\n\n')), // Heartbeat
|
|
94
|
+
new Uint8Array(new TextEncoder().encode('data: {"key": "value"}\n\n')),
|
|
95
|
+
];
|
|
96
|
+
const response = createMockResponse(chunks);
|
|
97
|
+
const selector = createSseSelector({ skipHeartbeats: true });
|
|
98
|
+
const events = await lastValueFrom(
|
|
99
|
+
of(response).pipe(
|
|
100
|
+
concatMap(selector),
|
|
101
|
+
scan((acc, event) => [...acc, event], [] as any[]),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(events).toEqual([{ data: { key: 'value' } }]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('retries on retry event with specified delay', async () => {
|
|
109
|
+
const chunks = [
|
|
110
|
+
new Uint8Array(new TextEncoder().encode('data: {"foo": "value1"}\n\n')),
|
|
111
|
+
new Uint8Array(new TextEncoder().encode('retry: 100\n\n')),
|
|
112
|
+
new Uint8Array(new TextEncoder().encode('data: {"bar": "value2"}\n\n')),
|
|
113
|
+
];
|
|
114
|
+
const response = createMockResponse(chunks);
|
|
115
|
+
const selector = createSseSelector();
|
|
116
|
+
|
|
117
|
+
const events = await lastValueFrom(
|
|
118
|
+
of(response).pipe(
|
|
119
|
+
concatMap(selector),
|
|
120
|
+
scan((acc, event) => [...acc, event], [] as any[]),
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(events).toEqual([{ data: { foo: 'value1' } }, { data: { bar: 'value2' } }]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('stops on abort signal', async () => {
|
|
128
|
+
const abortController = new AbortController();
|
|
129
|
+
const chunks = [
|
|
130
|
+
new Uint8Array(new TextEncoder().encode('data: {"key": "value"}\n\n')),
|
|
131
|
+
new Uint8Array(new TextEncoder().encode('retry: 100\n\n')),
|
|
132
|
+
new Uint8Array(new TextEncoder().encode('data: {"next": "event"}\n\n')),
|
|
133
|
+
];
|
|
134
|
+
const response = createMockResponse(chunks);
|
|
135
|
+
const selector = createSseSelector({ abortSignal: abortController.signal });
|
|
136
|
+
const eventsPromise = lastValueFrom(
|
|
137
|
+
of(response).pipe(
|
|
138
|
+
concatMap(selector),
|
|
139
|
+
scan((acc, event) => [...acc, event], [] as any[]),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
144
|
+
abortController.abort();
|
|
145
|
+
|
|
146
|
+
const events = await eventsPromise;
|
|
147
|
+
|
|
148
|
+
expect(events).toEqual([{ data: { key: 'value' } }]);
|
|
149
|
+
});
|
|
150
|
+
});
|