rx-player 4.5.0-dev.2026012302 → 4.5.0-dev.2026031700
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 +21 -3
- package/VERSION +1 -1
- package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
- package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
- package/dist/commonjs/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
- package/dist/commonjs/__GENERATED_CODE/embedded_worker.js +1 -1
- package/dist/commonjs/compat/should_seek_at_zero.d.ts +12 -0
- package/dist/commonjs/compat/should_seek_at_zero.d.ts.map +1 -0
- package/dist/commonjs/compat/should_seek_at_zero.js +23 -0
- package/dist/commonjs/core/cmcd/cmcd_data_builder.js +2 -2
- package/dist/commonjs/core/entry/FreezeResolver.js +2 -2
- package/dist/commonjs/core/entry/content_preparer.d.ts +1 -6
- package/dist/commonjs/core/entry/content_preparer.d.ts.map +1 -1
- package/dist/commonjs/core/entry/content_preparer.js +2 -125
- package/dist/commonjs/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
- package/dist/commonjs/core/entry/content_time_boundaries_observer.js +8 -0
- package/dist/commonjs/core/entry/core_entry.d.ts +1 -1
- package/dist/commonjs/core/entry/core_entry.d.ts.map +1 -1
- package/dist/commonjs/core/entry/core_entry.js +2 -7
- package/dist/commonjs/core/entry/core_text_displayer_interface.d.ts.map +1 -1
- package/dist/commonjs/core/entry/core_text_displayer_interface.js +3 -1
- package/dist/commonjs/core/entry/index.d.ts +1 -1
- package/dist/commonjs/core/entry/index.d.ts.map +1 -1
- package/dist/commonjs/core/entry/utils.d.ts +64 -0
- package/dist/commonjs/core/entry/utils.d.ts.map +1 -1
- package/dist/commonjs/core/entry/utils.js +165 -0
- package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts +5 -6
- package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts.map +1 -1
- package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
- package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.js +34 -30
- package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts +70 -0
- package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
- package/dist/commonjs/core/stream/representation/encryption_data_notifier.js +107 -0
- package/dist/commonjs/core/stream/representation/representation_stream.d.ts.map +1 -1
- package/dist/commonjs/core/stream/representation/representation_stream.js +12 -60
- package/dist/commonjs/core/stream/representation/utils/get_needed_segments.js +1 -1
- package/dist/commonjs/default_config.d.ts +5 -0
- package/dist/commonjs/default_config.d.ts.map +1 -1
- package/dist/commonjs/default_config.js +5 -0
- package/dist/commonjs/experimental/features/worker/index.d.ts +2 -1
- package/dist/commonjs/experimental/features/worker/index.d.ts.map +1 -1
- package/dist/commonjs/experimental/features/worker/index.js +4 -3
- package/dist/commonjs/main_thread/api/public_api.d.ts.map +1 -1
- package/dist/commonjs/main_thread/api/public_api.js +28 -4
- package/dist/commonjs/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
- package/dist/commonjs/main_thread/init/directfile_content_initializer.js +23 -1
- package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
- package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.js +3 -1
- package/dist/commonjs/manifest/classes/index.d.ts +2 -1
- package/dist/commonjs/manifest/classes/index.d.ts.map +1 -1
- package/dist/commonjs/manifest/classes/representation.d.ts +1 -1
- package/dist/commonjs/manifest/classes/representation.d.ts.map +1 -1
- package/dist/commonjs/manifest/classes/representation.js +2 -2
- package/dist/commonjs/manifest/classes/representation_index/types.d.ts +2 -2
- package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
- package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
- package/dist/commonjs/utils/base64.d.ts +15 -1
- package/dist/commonjs/utils/base64.d.ts.map +1 -1
- package/dist/commonjs/utils/base64.js +6 -0
- package/dist/commonjs/utils/ranges.d.ts +1 -1
- package/dist/commonjs/utils/ranges.js +1 -1
- package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
- package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
- package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
- package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
- package/dist/es2017/compat/should_seek_at_zero.d.ts +12 -0
- package/dist/es2017/compat/should_seek_at_zero.d.ts.map +1 -0
- package/dist/es2017/compat/should_seek_at_zero.js +20 -0
- package/dist/es2017/core/cmcd/cmcd_data_builder.js +2 -2
- package/dist/es2017/core/entry/FreezeResolver.js +1 -1
- package/dist/es2017/core/entry/content_preparer.d.ts +1 -6
- package/dist/es2017/core/entry/content_preparer.d.ts.map +1 -1
- package/dist/es2017/core/entry/content_preparer.js +1 -76
- package/dist/es2017/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
- package/dist/es2017/core/entry/content_time_boundaries_observer.js +8 -0
- package/dist/es2017/core/entry/core_entry.d.ts +1 -1
- package/dist/es2017/core/entry/core_entry.d.ts.map +1 -1
- package/dist/es2017/core/entry/core_entry.js +2 -7
- package/dist/es2017/core/entry/core_text_displayer_interface.d.ts.map +1 -1
- package/dist/es2017/core/entry/core_text_displayer_interface.js +3 -1
- package/dist/es2017/core/entry/index.d.ts +1 -1
- package/dist/es2017/core/entry/index.d.ts.map +1 -1
- package/dist/es2017/core/entry/utils.d.ts +64 -0
- package/dist/es2017/core/entry/utils.d.ts.map +1 -1
- package/dist/es2017/core/entry/utils.js +98 -0
- package/dist/es2017/core/fetchers/segment/segment_queue.d.ts +5 -6
- package/dist/es2017/core/fetchers/segment/segment_queue.d.ts.map +1 -1
- package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
- package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.js +21 -16
- package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts +70 -0
- package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
- package/dist/es2017/core/stream/representation/encryption_data_notifier.js +80 -0
- package/dist/es2017/core/stream/representation/representation_stream.d.ts.map +1 -1
- package/dist/es2017/core/stream/representation/representation_stream.js +10 -38
- package/dist/es2017/core/stream/representation/utils/get_needed_segments.js +1 -1
- package/dist/es2017/default_config.d.ts +5 -0
- package/dist/es2017/default_config.d.ts.map +1 -1
- package/dist/es2017/default_config.js +5 -0
- package/dist/es2017/experimental/features/worker/index.d.ts +2 -1
- package/dist/es2017/experimental/features/worker/index.d.ts.map +1 -1
- package/dist/es2017/experimental/features/worker/index.js +2 -3
- package/dist/es2017/main_thread/api/public_api.d.ts.map +1 -1
- package/dist/es2017/main_thread/api/public_api.js +28 -4
- package/dist/es2017/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
- package/dist/es2017/main_thread/init/directfile_content_initializer.js +23 -1
- package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
- package/dist/es2017/main_thread/init/utils/initial_seek_and_play.js +3 -1
- package/dist/es2017/manifest/classes/index.d.ts +2 -1
- package/dist/es2017/manifest/classes/index.d.ts.map +1 -1
- package/dist/es2017/manifest/classes/representation.d.ts +1 -1
- package/dist/es2017/manifest/classes/representation.d.ts.map +1 -1
- package/dist/es2017/manifest/classes/representation.js +1 -1
- package/dist/es2017/manifest/classes/representation_index/types.d.ts +2 -2
- package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
- package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
- package/dist/es2017/utils/base64.d.ts +15 -1
- package/dist/es2017/utils/base64.d.ts.map +1 -1
- package/dist/es2017/utils/base64.js +6 -0
- package/dist/es2017/utils/ranges.d.ts +1 -1
- package/dist/es2017/utils/ranges.js +1 -1
- package/dist/mpd-parser.wasm +0 -0
- package/dist/worker.js +8 -8
- package/package.json +26 -23
- package/src/__GENERATED_CODE/embedded_dash_wasm.ts +1 -1
- package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
- package/src/compat/should_seek_at_zero.ts +23 -0
- package/src/core/adaptive/__tests__/adaptive_representation_selector.test.ts +734 -0
- package/src/core/adaptive/__tests__/guess_based_chooser.test.ts +793 -0
- package/src/core/adaptive/__tests__/network_analyzer.test.ts +514 -0
- package/src/core/adaptive/utils/__tests__/bandwidth_estimator.test.ts +117 -0
- package/src/core/adaptive/utils/__tests__/last_estimate_storage.test.ts +76 -0
- package/src/core/adaptive/utils/__tests__/pending_requests_store.test.ts +288 -0
- package/src/core/adaptive/utils/__tests__/representation_score_calculator.test.ts +249 -0
- package/src/core/cmcd/__tests__/cmcd_data_builder.test.ts +464 -0
- package/src/core/cmcd/cmcd_data_builder.ts +2 -2
- package/src/core/entry/FreezeResolver.ts +1 -1
- package/src/core/entry/__tests__/FreezeResolver.test.ts +542 -0
- package/src/core/entry/__tests__/content_time_boundaries_observer.test.ts +867 -0
- package/src/core/entry/__tests__/core_text_displayer_interface.test.ts +205 -0
- package/src/core/entry/__tests__/get_buffered_data_per_media_buffer.test.ts +95 -0
- package/src/core/entry/__tests__/track_choice_setter.test.ts +288 -0
- package/src/core/entry/__tests__/utils.test.ts +309 -0
- package/src/core/entry/content_preparer.ts +7 -118
- package/src/core/entry/content_time_boundaries_observer.ts +10 -0
- package/src/core/entry/core_entry.ts +7 -14
- package/src/core/entry/core_text_displayer_interface.ts +3 -1
- package/src/core/entry/index.ts +1 -1
- package/src/core/entry/utils.ts +151 -0
- package/src/core/fetchers/__tests__/cdn_prioritizer.test.ts +166 -0
- package/src/core/fetchers/manifest/__tests__/manifest_fetcher.test.ts +803 -0
- package/src/core/fetchers/segment/__tests__/segment_fetcher.test.ts +872 -0
- package/src/core/fetchers/segment/__tests__/segment_queue.test.ts +557 -0
- package/src/core/fetchers/segment/segment_queue.ts +5 -6
- package/src/core/fetchers/thumbnails/__tests__/thumbnail_fetcher.test.ts +435 -0
- package/src/core/fetchers/thumbnails/thumbnail_fetcher.ts +28 -20
- package/src/core/fetchers/utils/__tests__/schedule_request.test.ts +331 -0
- package/src/core/stream/adaptation/__tests__/get_representations_switch_strategy.test.ts +593 -0
- package/src/core/stream/period/utils/__tests__/get_adaptation_switch_strategy.test.ts +646 -0
- package/src/core/stream/representation/__tests__/encryption_data_notifier.test.ts +531 -0
- package/src/core/stream/representation/encryption_data_notifier.ts +122 -0
- package/src/core/stream/representation/representation_stream.ts +11 -49
- package/src/core/stream/representation/utils/__tests__/append_segment_to_buffer.test.ts +321 -0
- package/src/core/stream/representation/utils/__tests__/check_for_discontinuity.test.ts +513 -0
- package/src/core/stream/representation/utils/__tests__/get_segment_priority.test.ts +132 -0
- package/src/core/stream/representation/utils/__tests__/push_init_segment.test.ts +209 -0
- package/src/core/stream/representation/utils/__tests__/push_media_segment.test.ts +343 -0
- package/src/core/stream/representation/utils/get_needed_segments.ts +1 -1
- package/src/default_config.ts +6 -0
- package/src/experimental/features/worker/index.ts +2 -4
- package/src/main_thread/api/public_api.ts +33 -4
- package/src/main_thread/init/directfile_content_initializer.ts +26 -1
- package/src/main_thread/init/utils/initial_seek_and_play.ts +5 -1
- package/src/manifest/classes/index.ts +2 -0
- package/src/manifest/classes/representation.ts +2 -1
- package/src/manifest/classes/representation_index/types.ts +2 -2
- package/src/mse/__tests__/main_media_source_interface.test.ts +846 -0
- package/src/parsers/texttracks/ttml/html/apply_line_height.ts +16 -1
- package/src/utils/__tests__/base64.test.ts +159 -191
- package/src/utils/base64.ts +30 -1
- package/src/utils/ranges.ts +1 -1
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { NetworkErrorTypes } from "../../../../errors";
|
|
3
|
+
import { RequestError } from "../../../../utils/request";
|
|
4
|
+
import TaskCanceller from "../../../../utils/task_canceller";
|
|
5
|
+
import { scheduleRequestWithCdns, scheduleRequestPromise } from "../schedule_request";
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
9
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
10
|
+
|
|
11
|
+
const { mockLog, mockCancellableSleep, mockGetFuzzedDelay, mockGetTimestamp } =
|
|
12
|
+
vi.hoisted(() => {
|
|
13
|
+
return {
|
|
14
|
+
mockLog: { warn: vi.fn() },
|
|
15
|
+
mockCancellableSleep: vi.fn(),
|
|
16
|
+
mockGetFuzzedDelay: vi.fn((delay: number) => delay),
|
|
17
|
+
mockGetTimestamp: vi.fn(() => 0),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
vi.mock("../../../log", () => ({ default: mockLog }));
|
|
22
|
+
vi.mock("../../../utils/cancellable_sleep", () => ({ default: mockCancellableSleep }));
|
|
23
|
+
vi.mock("../../../utils/get_fuzzed_delay", () => ({ default: mockGetFuzzedDelay }));
|
|
24
|
+
vi.mock("../../../utils/monotonic_timestamp", () => ({ default: mockGetTimestamp }));
|
|
25
|
+
|
|
26
|
+
const defaultOptions = {
|
|
27
|
+
baseDelay: 100,
|
|
28
|
+
maxDelay: 2000,
|
|
29
|
+
maxRetry: 3,
|
|
30
|
+
onRetry: vi.fn(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function makeCdn(id: string): any {
|
|
34
|
+
return { baseUrl: id };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
mockGetTimestamp.mockReturnValue(0);
|
|
39
|
+
mockGetFuzzedDelay.mockImplementation((delay: number) => delay);
|
|
40
|
+
// By default cancellableSleep resolves immediately
|
|
41
|
+
mockCancellableSleep.mockResolvedValue(undefined);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
vi.resetAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe.skip("scheduleRequestWithCdns", () => {
|
|
49
|
+
it("resolves with the result of a successful request", async () => {
|
|
50
|
+
const cdn = makeCdn("https://cdn1.example.com");
|
|
51
|
+
const performRequest = vi.fn().mockResolvedValue("ok");
|
|
52
|
+
const result = await scheduleRequestWithCdns(
|
|
53
|
+
[cdn],
|
|
54
|
+
null,
|
|
55
|
+
performRequest,
|
|
56
|
+
defaultOptions,
|
|
57
|
+
new TaskCanceller("test").signal,
|
|
58
|
+
);
|
|
59
|
+
expect(result).toBe("ok");
|
|
60
|
+
expect(performRequest).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(performRequest).toHaveBeenCalledWith(cdn, expect.anything());
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("passes null cdn when cdns param is null", async () => {
|
|
65
|
+
const performRequest = vi.fn().mockResolvedValue("data");
|
|
66
|
+
await scheduleRequestWithCdns(
|
|
67
|
+
null,
|
|
68
|
+
null,
|
|
69
|
+
performRequest,
|
|
70
|
+
defaultOptions,
|
|
71
|
+
new TaskCanceller("test").signal,
|
|
72
|
+
);
|
|
73
|
+
expect(performRequest).toHaveBeenCalledWith(null, expect.anything());
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("throws immediately if cancellationSignal is already cancelled", async () => {
|
|
77
|
+
const cancellationError = new Error("cancelled");
|
|
78
|
+
const signal: any = {
|
|
79
|
+
cancellationError,
|
|
80
|
+
isCancelled: () => true,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await expect(
|
|
84
|
+
scheduleRequestWithCdns([makeCdn("a")], null, vi.fn(), defaultOptions, signal),
|
|
85
|
+
).rejects.toThrow("cancelled");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("throws 'No CDN to request' when given an empty CDN array", async () => {
|
|
89
|
+
await expect(
|
|
90
|
+
scheduleRequestWithCdns(
|
|
91
|
+
[],
|
|
92
|
+
null,
|
|
93
|
+
vi.fn(),
|
|
94
|
+
defaultOptions,
|
|
95
|
+
new TaskCanceller("test").signal,
|
|
96
|
+
),
|
|
97
|
+
).rejects.toThrow("No CDN to request");
|
|
98
|
+
expect(mockLog.warn).toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("retries on a 500 HTTP error and resolves on second attempt", async () => {
|
|
102
|
+
const cdn = makeCdn("https://cdn1.example.com");
|
|
103
|
+
const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
104
|
+
const performRequest = vi
|
|
105
|
+
.fn()
|
|
106
|
+
.mockRejectedValueOnce(error)
|
|
107
|
+
.mockResolvedValueOnce("recovered");
|
|
108
|
+
|
|
109
|
+
const onRetry = vi.fn();
|
|
110
|
+
const result = await scheduleRequestWithCdns(
|
|
111
|
+
[cdn],
|
|
112
|
+
null,
|
|
113
|
+
performRequest,
|
|
114
|
+
{ ...defaultOptions, onRetry },
|
|
115
|
+
new TaskCanceller("test").signal,
|
|
116
|
+
);
|
|
117
|
+
expect(result).toBe("recovered");
|
|
118
|
+
expect(onRetry).toHaveBeenCalledTimes(1);
|
|
119
|
+
expect(onRetry).toHaveBeenCalledWith(error);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("retries on TIMEOUT error", async () => {
|
|
123
|
+
const cdn = makeCdn("https://cdn1.example.com");
|
|
124
|
+
const error = new RequestError("url", 0, NetworkErrorTypes.TIMEOUT);
|
|
125
|
+
const performRequest = vi
|
|
126
|
+
.fn()
|
|
127
|
+
.mockRejectedValueOnce(error)
|
|
128
|
+
.mockResolvedValueOnce("ok");
|
|
129
|
+
const result = await scheduleRequestWithCdns(
|
|
130
|
+
[cdn],
|
|
131
|
+
null,
|
|
132
|
+
performRequest,
|
|
133
|
+
defaultOptions,
|
|
134
|
+
new TaskCanceller("test").signal,
|
|
135
|
+
);
|
|
136
|
+
expect(result).toBe("ok");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("does NOT retry on a 403 HTTP error and rejects immediately", async () => {
|
|
140
|
+
const cdn = makeCdn("https://cdn1.example.com");
|
|
141
|
+
const error = new RequestError("url", 403, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
142
|
+
const performRequest = vi.fn().mockRejectedValue(error);
|
|
143
|
+
const onRetry = vi.fn();
|
|
144
|
+
|
|
145
|
+
await expect(
|
|
146
|
+
scheduleRequestWithCdns(
|
|
147
|
+
[cdn],
|
|
148
|
+
null,
|
|
149
|
+
performRequest,
|
|
150
|
+
{ ...defaultOptions, maxRetry: 5, onRetry },
|
|
151
|
+
new TaskCanceller("test").signal,
|
|
152
|
+
),
|
|
153
|
+
).rejects.toThrow();
|
|
154
|
+
// 403 should not be retried — performRequest called once only
|
|
155
|
+
expect(performRequest).toHaveBeenCalledTimes(1);
|
|
156
|
+
expect(onRetry).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("blacklists a CDN after maxRetry exhaustion and rejects", async () => {
|
|
160
|
+
const cdn = makeCdn("https://cdn1.example.com");
|
|
161
|
+
const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
162
|
+
const performRequest = vi.fn().mockRejectedValue(error);
|
|
163
|
+
const onRetry = vi.fn();
|
|
164
|
+
const maxRetry = 2;
|
|
165
|
+
|
|
166
|
+
await expect(
|
|
167
|
+
scheduleRequestWithCdns(
|
|
168
|
+
[cdn],
|
|
169
|
+
null,
|
|
170
|
+
performRequest,
|
|
171
|
+
{ ...defaultOptions, maxRetry, onRetry },
|
|
172
|
+
new TaskCanceller("test").signal,
|
|
173
|
+
),
|
|
174
|
+
).rejects.toThrow();
|
|
175
|
+
|
|
176
|
+
// initial attempt + maxRetry retries = maxRetry+1 calls
|
|
177
|
+
expect(performRequest).toHaveBeenCalledTimes(maxRetry + 1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("falls over to next CDN when first CDN fails with a non-retryable error", async () => {
|
|
181
|
+
const cdn1 = makeCdn("https://cdn1.example.com");
|
|
182
|
+
const cdn2 = makeCdn("https://cdn2.example.com");
|
|
183
|
+
// 403 is non-retryable → should move to cdn2
|
|
184
|
+
const error403 = new RequestError("url", 403, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
185
|
+
const performRequest = vi
|
|
186
|
+
.fn()
|
|
187
|
+
.mockRejectedValueOnce(error403)
|
|
188
|
+
.mockResolvedValueOnce("cdn2-response");
|
|
189
|
+
|
|
190
|
+
const result = await scheduleRequestWithCdns(
|
|
191
|
+
[cdn1, cdn2],
|
|
192
|
+
null,
|
|
193
|
+
performRequest,
|
|
194
|
+
defaultOptions,
|
|
195
|
+
new TaskCanceller("test").signal,
|
|
196
|
+
);
|
|
197
|
+
expect(result).toBe("cdn2-response");
|
|
198
|
+
expect(performRequest).toHaveBeenNthCalledWith(1, cdn1, expect.anything());
|
|
199
|
+
expect(performRequest).toHaveBeenNthCalledWith(2, cdn2, expect.anything());
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("rejects with the last error when all CDNs are exhausted", async () => {
|
|
203
|
+
const cdn1 = makeCdn("cdn1");
|
|
204
|
+
const cdn2 = makeCdn("cdn2");
|
|
205
|
+
const error1 = new RequestError("url1", 403, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
206
|
+
const error2 = new RequestError("url2", 403, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
207
|
+
const performRequest = vi
|
|
208
|
+
.fn()
|
|
209
|
+
.mockRejectedValueOnce(error1)
|
|
210
|
+
.mockRejectedValueOnce(error2);
|
|
211
|
+
|
|
212
|
+
await expect(
|
|
213
|
+
scheduleRequestWithCdns(
|
|
214
|
+
[cdn1, cdn2],
|
|
215
|
+
null,
|
|
216
|
+
performRequest,
|
|
217
|
+
defaultOptions,
|
|
218
|
+
new TaskCanceller("test").signal,
|
|
219
|
+
),
|
|
220
|
+
).rejects.toThrow();
|
|
221
|
+
expect(performRequest).toHaveBeenCalledTimes(2);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("downgrades CDN in prioritizer after failure", async () => {
|
|
225
|
+
const cdn = makeCdn("cdn1");
|
|
226
|
+
const error = new RequestError("url", 403, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
227
|
+
const cdnPrioritizer = {
|
|
228
|
+
getCdnPreferenceForResource: vi.fn().mockReturnValue([cdn]),
|
|
229
|
+
downgradeCdn: vi.fn(),
|
|
230
|
+
addEventListener: vi.fn(),
|
|
231
|
+
};
|
|
232
|
+
const performRequest = vi.fn().mockRejectedValue(error);
|
|
233
|
+
|
|
234
|
+
await expect(
|
|
235
|
+
scheduleRequestWithCdns(
|
|
236
|
+
[cdn],
|
|
237
|
+
cdnPrioritizer as never,
|
|
238
|
+
performRequest,
|
|
239
|
+
defaultOptions,
|
|
240
|
+
new TaskCanceller("test").signal,
|
|
241
|
+
),
|
|
242
|
+
).rejects.toThrow();
|
|
243
|
+
|
|
244
|
+
expect(cdnPrioritizer.downgradeCdn).toHaveBeenCalledWith(cdn);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("retries on INTEGRITY_ERROR", async () => {
|
|
248
|
+
const cdn = makeCdn("cdn1");
|
|
249
|
+
const integrityError = { code: "INTEGRITY_ERROR" };
|
|
250
|
+
const performRequest = vi
|
|
251
|
+
.fn()
|
|
252
|
+
.mockRejectedValueOnce(integrityError)
|
|
253
|
+
.mockResolvedValueOnce("ok");
|
|
254
|
+
|
|
255
|
+
const result = await scheduleRequestWithCdns(
|
|
256
|
+
[cdn],
|
|
257
|
+
null,
|
|
258
|
+
performRequest,
|
|
259
|
+
{ ...defaultOptions, maxRetry: 2 },
|
|
260
|
+
new TaskCanceller("test").signal,
|
|
261
|
+
);
|
|
262
|
+
expect(result).toBe("ok");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("computes exponential backoff delay capped at maxDelay", async () => {
|
|
266
|
+
const cdn = makeCdn("cdn1");
|
|
267
|
+
const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
268
|
+
// Fail twice then succeed
|
|
269
|
+
const performRequest = vi
|
|
270
|
+
.fn()
|
|
271
|
+
.mockRejectedValueOnce(error)
|
|
272
|
+
.mockRejectedValueOnce(error)
|
|
273
|
+
.mockResolvedValueOnce("done");
|
|
274
|
+
|
|
275
|
+
const baseDelay = 100;
|
|
276
|
+
const maxDelay = 150;
|
|
277
|
+
|
|
278
|
+
await scheduleRequestWithCdns(
|
|
279
|
+
[cdn],
|
|
280
|
+
null,
|
|
281
|
+
performRequest,
|
|
282
|
+
{ baseDelay, maxDelay, maxRetry: 5, onRetry: vi.fn() },
|
|
283
|
+
new TaskCanceller("test").signal,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// After 1st failure: delay = min(100 * 2^0, 150) = 100
|
|
287
|
+
// After 2nd failure: delay = min(100 * 2^1, 150) = 150
|
|
288
|
+
expect(mockGetFuzzedDelay).toHaveBeenNthCalledWith(1, 100);
|
|
289
|
+
expect(mockGetFuzzedDelay).toHaveBeenNthCalledWith(2, 150);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe("scheduleRequestPromise", () => {
|
|
294
|
+
it("resolves with the performRequest result", async () => {
|
|
295
|
+
const performRequest = vi.fn().mockResolvedValue("result");
|
|
296
|
+
const result = await scheduleRequestPromise(
|
|
297
|
+
performRequest,
|
|
298
|
+
defaultOptions,
|
|
299
|
+
new TaskCanceller("test").signal,
|
|
300
|
+
);
|
|
301
|
+
expect(result).toBe("result");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("retries and resolves after transient failure", async () => {
|
|
305
|
+
const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
306
|
+
const performRequest = vi
|
|
307
|
+
.fn()
|
|
308
|
+
.mockRejectedValueOnce(error)
|
|
309
|
+
.mockResolvedValueOnce("ok");
|
|
310
|
+
const result = await scheduleRequestPromise(
|
|
311
|
+
performRequest,
|
|
312
|
+
{ ...defaultOptions, maxRetry: 2 },
|
|
313
|
+
new TaskCanceller("test").signal,
|
|
314
|
+
);
|
|
315
|
+
expect(result).toBe("ok");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("rejects after all retries are exhausted", async () => {
|
|
319
|
+
const error = new RequestError("url", 500, NetworkErrorTypes.ERROR_HTTP_CODE);
|
|
320
|
+
const performRequest = vi.fn().mockRejectedValue(error);
|
|
321
|
+
await expect(
|
|
322
|
+
scheduleRequestPromise(
|
|
323
|
+
performRequest,
|
|
324
|
+
{ ...defaultOptions, maxRetry: 1 },
|
|
325
|
+
new TaskCanceller("test").signal,
|
|
326
|
+
),
|
|
327
|
+
).rejects.toThrow();
|
|
328
|
+
// 1 initial + 1 retry = 2 total
|
|
329
|
+
expect(performRequest).toHaveBeenCalledTimes(2);
|
|
330
|
+
});
|
|
331
|
+
});
|