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.
Files changed (180) hide show
  1. package/CHANGELOG.md +21 -3
  2. package/VERSION +1 -1
  3. package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  4. package/dist/commonjs/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  5. package/dist/commonjs/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  6. package/dist/commonjs/__GENERATED_CODE/embedded_worker.js +1 -1
  7. package/dist/commonjs/compat/should_seek_at_zero.d.ts +12 -0
  8. package/dist/commonjs/compat/should_seek_at_zero.d.ts.map +1 -0
  9. package/dist/commonjs/compat/should_seek_at_zero.js +23 -0
  10. package/dist/commonjs/core/cmcd/cmcd_data_builder.js +2 -2
  11. package/dist/commonjs/core/entry/FreezeResolver.js +2 -2
  12. package/dist/commonjs/core/entry/content_preparer.d.ts +1 -6
  13. package/dist/commonjs/core/entry/content_preparer.d.ts.map +1 -1
  14. package/dist/commonjs/core/entry/content_preparer.js +2 -125
  15. package/dist/commonjs/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
  16. package/dist/commonjs/core/entry/content_time_boundaries_observer.js +8 -0
  17. package/dist/commonjs/core/entry/core_entry.d.ts +1 -1
  18. package/dist/commonjs/core/entry/core_entry.d.ts.map +1 -1
  19. package/dist/commonjs/core/entry/core_entry.js +2 -7
  20. package/dist/commonjs/core/entry/core_text_displayer_interface.d.ts.map +1 -1
  21. package/dist/commonjs/core/entry/core_text_displayer_interface.js +3 -1
  22. package/dist/commonjs/core/entry/index.d.ts +1 -1
  23. package/dist/commonjs/core/entry/index.d.ts.map +1 -1
  24. package/dist/commonjs/core/entry/utils.d.ts +64 -0
  25. package/dist/commonjs/core/entry/utils.d.ts.map +1 -1
  26. package/dist/commonjs/core/entry/utils.js +165 -0
  27. package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts +5 -6
  28. package/dist/commonjs/core/fetchers/segment/segment_queue.d.ts.map +1 -1
  29. package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
  30. package/dist/commonjs/core/fetchers/thumbnails/thumbnail_fetcher.js +34 -30
  31. package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts +70 -0
  32. package/dist/commonjs/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
  33. package/dist/commonjs/core/stream/representation/encryption_data_notifier.js +107 -0
  34. package/dist/commonjs/core/stream/representation/representation_stream.d.ts.map +1 -1
  35. package/dist/commonjs/core/stream/representation/representation_stream.js +12 -60
  36. package/dist/commonjs/core/stream/representation/utils/get_needed_segments.js +1 -1
  37. package/dist/commonjs/default_config.d.ts +5 -0
  38. package/dist/commonjs/default_config.d.ts.map +1 -1
  39. package/dist/commonjs/default_config.js +5 -0
  40. package/dist/commonjs/experimental/features/worker/index.d.ts +2 -1
  41. package/dist/commonjs/experimental/features/worker/index.d.ts.map +1 -1
  42. package/dist/commonjs/experimental/features/worker/index.js +4 -3
  43. package/dist/commonjs/main_thread/api/public_api.d.ts.map +1 -1
  44. package/dist/commonjs/main_thread/api/public_api.js +28 -4
  45. package/dist/commonjs/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  46. package/dist/commonjs/main_thread/init/directfile_content_initializer.js +23 -1
  47. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  48. package/dist/commonjs/main_thread/init/utils/initial_seek_and_play.js +3 -1
  49. package/dist/commonjs/manifest/classes/index.d.ts +2 -1
  50. package/dist/commonjs/manifest/classes/index.d.ts.map +1 -1
  51. package/dist/commonjs/manifest/classes/representation.d.ts +1 -1
  52. package/dist/commonjs/manifest/classes/representation.d.ts.map +1 -1
  53. package/dist/commonjs/manifest/classes/representation.js +2 -2
  54. package/dist/commonjs/manifest/classes/representation_index/types.d.ts +2 -2
  55. package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
  56. package/dist/commonjs/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
  57. package/dist/commonjs/utils/base64.d.ts +15 -1
  58. package/dist/commonjs/utils/base64.d.ts.map +1 -1
  59. package/dist/commonjs/utils/base64.js +6 -0
  60. package/dist/commonjs/utils/ranges.d.ts +1 -1
  61. package/dist/commonjs/utils/ranges.js +1 -1
  62. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.d.ts.map +1 -1
  63. package/dist/es2017/__GENERATED_CODE/embedded_dash_wasm.js +1 -1
  64. package/dist/es2017/__GENERATED_CODE/embedded_worker.d.ts.map +1 -1
  65. package/dist/es2017/__GENERATED_CODE/embedded_worker.js +1 -1
  66. package/dist/es2017/compat/should_seek_at_zero.d.ts +12 -0
  67. package/dist/es2017/compat/should_seek_at_zero.d.ts.map +1 -0
  68. package/dist/es2017/compat/should_seek_at_zero.js +20 -0
  69. package/dist/es2017/core/cmcd/cmcd_data_builder.js +2 -2
  70. package/dist/es2017/core/entry/FreezeResolver.js +1 -1
  71. package/dist/es2017/core/entry/content_preparer.d.ts +1 -6
  72. package/dist/es2017/core/entry/content_preparer.d.ts.map +1 -1
  73. package/dist/es2017/core/entry/content_preparer.js +1 -76
  74. package/dist/es2017/core/entry/content_time_boundaries_observer.d.ts.map +1 -1
  75. package/dist/es2017/core/entry/content_time_boundaries_observer.js +8 -0
  76. package/dist/es2017/core/entry/core_entry.d.ts +1 -1
  77. package/dist/es2017/core/entry/core_entry.d.ts.map +1 -1
  78. package/dist/es2017/core/entry/core_entry.js +2 -7
  79. package/dist/es2017/core/entry/core_text_displayer_interface.d.ts.map +1 -1
  80. package/dist/es2017/core/entry/core_text_displayer_interface.js +3 -1
  81. package/dist/es2017/core/entry/index.d.ts +1 -1
  82. package/dist/es2017/core/entry/index.d.ts.map +1 -1
  83. package/dist/es2017/core/entry/utils.d.ts +64 -0
  84. package/dist/es2017/core/entry/utils.d.ts.map +1 -1
  85. package/dist/es2017/core/entry/utils.js +98 -0
  86. package/dist/es2017/core/fetchers/segment/segment_queue.d.ts +5 -6
  87. package/dist/es2017/core/fetchers/segment/segment_queue.d.ts.map +1 -1
  88. package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.d.ts.map +1 -1
  89. package/dist/es2017/core/fetchers/thumbnails/thumbnail_fetcher.js +21 -16
  90. package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts +70 -0
  91. package/dist/es2017/core/stream/representation/encryption_data_notifier.d.ts.map +1 -0
  92. package/dist/es2017/core/stream/representation/encryption_data_notifier.js +80 -0
  93. package/dist/es2017/core/stream/representation/representation_stream.d.ts.map +1 -1
  94. package/dist/es2017/core/stream/representation/representation_stream.js +10 -38
  95. package/dist/es2017/core/stream/representation/utils/get_needed_segments.js +1 -1
  96. package/dist/es2017/default_config.d.ts +5 -0
  97. package/dist/es2017/default_config.d.ts.map +1 -1
  98. package/dist/es2017/default_config.js +5 -0
  99. package/dist/es2017/experimental/features/worker/index.d.ts +2 -1
  100. package/dist/es2017/experimental/features/worker/index.d.ts.map +1 -1
  101. package/dist/es2017/experimental/features/worker/index.js +2 -3
  102. package/dist/es2017/main_thread/api/public_api.d.ts.map +1 -1
  103. package/dist/es2017/main_thread/api/public_api.js +28 -4
  104. package/dist/es2017/main_thread/init/directfile_content_initializer.d.ts.map +1 -1
  105. package/dist/es2017/main_thread/init/directfile_content_initializer.js +23 -1
  106. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.d.ts.map +1 -1
  107. package/dist/es2017/main_thread/init/utils/initial_seek_and_play.js +3 -1
  108. package/dist/es2017/manifest/classes/index.d.ts +2 -1
  109. package/dist/es2017/manifest/classes/index.d.ts.map +1 -1
  110. package/dist/es2017/manifest/classes/representation.d.ts +1 -1
  111. package/dist/es2017/manifest/classes/representation.d.ts.map +1 -1
  112. package/dist/es2017/manifest/classes/representation.js +1 -1
  113. package/dist/es2017/manifest/classes/representation_index/types.d.ts +2 -2
  114. package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.d.ts.map +1 -1
  115. package/dist/es2017/parsers/texttracks/ttml/html/apply_line_height.js +16 -1
  116. package/dist/es2017/utils/base64.d.ts +15 -1
  117. package/dist/es2017/utils/base64.d.ts.map +1 -1
  118. package/dist/es2017/utils/base64.js +6 -0
  119. package/dist/es2017/utils/ranges.d.ts +1 -1
  120. package/dist/es2017/utils/ranges.js +1 -1
  121. package/dist/mpd-parser.wasm +0 -0
  122. package/dist/worker.js +8 -8
  123. package/package.json +26 -23
  124. package/src/__GENERATED_CODE/embedded_dash_wasm.ts +1 -1
  125. package/src/__GENERATED_CODE/embedded_worker.ts +1 -1
  126. package/src/compat/should_seek_at_zero.ts +23 -0
  127. package/src/core/adaptive/__tests__/adaptive_representation_selector.test.ts +734 -0
  128. package/src/core/adaptive/__tests__/guess_based_chooser.test.ts +793 -0
  129. package/src/core/adaptive/__tests__/network_analyzer.test.ts +514 -0
  130. package/src/core/adaptive/utils/__tests__/bandwidth_estimator.test.ts +117 -0
  131. package/src/core/adaptive/utils/__tests__/last_estimate_storage.test.ts +76 -0
  132. package/src/core/adaptive/utils/__tests__/pending_requests_store.test.ts +288 -0
  133. package/src/core/adaptive/utils/__tests__/representation_score_calculator.test.ts +249 -0
  134. package/src/core/cmcd/__tests__/cmcd_data_builder.test.ts +464 -0
  135. package/src/core/cmcd/cmcd_data_builder.ts +2 -2
  136. package/src/core/entry/FreezeResolver.ts +1 -1
  137. package/src/core/entry/__tests__/FreezeResolver.test.ts +542 -0
  138. package/src/core/entry/__tests__/content_time_boundaries_observer.test.ts +867 -0
  139. package/src/core/entry/__tests__/core_text_displayer_interface.test.ts +205 -0
  140. package/src/core/entry/__tests__/get_buffered_data_per_media_buffer.test.ts +95 -0
  141. package/src/core/entry/__tests__/track_choice_setter.test.ts +288 -0
  142. package/src/core/entry/__tests__/utils.test.ts +309 -0
  143. package/src/core/entry/content_preparer.ts +7 -118
  144. package/src/core/entry/content_time_boundaries_observer.ts +10 -0
  145. package/src/core/entry/core_entry.ts +7 -14
  146. package/src/core/entry/core_text_displayer_interface.ts +3 -1
  147. package/src/core/entry/index.ts +1 -1
  148. package/src/core/entry/utils.ts +151 -0
  149. package/src/core/fetchers/__tests__/cdn_prioritizer.test.ts +166 -0
  150. package/src/core/fetchers/manifest/__tests__/manifest_fetcher.test.ts +803 -0
  151. package/src/core/fetchers/segment/__tests__/segment_fetcher.test.ts +872 -0
  152. package/src/core/fetchers/segment/__tests__/segment_queue.test.ts +557 -0
  153. package/src/core/fetchers/segment/segment_queue.ts +5 -6
  154. package/src/core/fetchers/thumbnails/__tests__/thumbnail_fetcher.test.ts +435 -0
  155. package/src/core/fetchers/thumbnails/thumbnail_fetcher.ts +28 -20
  156. package/src/core/fetchers/utils/__tests__/schedule_request.test.ts +331 -0
  157. package/src/core/stream/adaptation/__tests__/get_representations_switch_strategy.test.ts +593 -0
  158. package/src/core/stream/period/utils/__tests__/get_adaptation_switch_strategy.test.ts +646 -0
  159. package/src/core/stream/representation/__tests__/encryption_data_notifier.test.ts +531 -0
  160. package/src/core/stream/representation/encryption_data_notifier.ts +122 -0
  161. package/src/core/stream/representation/representation_stream.ts +11 -49
  162. package/src/core/stream/representation/utils/__tests__/append_segment_to_buffer.test.ts +321 -0
  163. package/src/core/stream/representation/utils/__tests__/check_for_discontinuity.test.ts +513 -0
  164. package/src/core/stream/representation/utils/__tests__/get_segment_priority.test.ts +132 -0
  165. package/src/core/stream/representation/utils/__tests__/push_init_segment.test.ts +209 -0
  166. package/src/core/stream/representation/utils/__tests__/push_media_segment.test.ts +343 -0
  167. package/src/core/stream/representation/utils/get_needed_segments.ts +1 -1
  168. package/src/default_config.ts +6 -0
  169. package/src/experimental/features/worker/index.ts +2 -4
  170. package/src/main_thread/api/public_api.ts +33 -4
  171. package/src/main_thread/init/directfile_content_initializer.ts +26 -1
  172. package/src/main_thread/init/utils/initial_seek_and_play.ts +5 -1
  173. package/src/manifest/classes/index.ts +2 -0
  174. package/src/manifest/classes/representation.ts +2 -1
  175. package/src/manifest/classes/representation_index/types.ts +2 -2
  176. package/src/mse/__tests__/main_media_source_interface.test.ts +846 -0
  177. package/src/parsers/texttracks/ttml/html/apply_line_height.ts +16 -1
  178. package/src/utils/__tests__/base64.test.ts +159 -191
  179. package/src/utils/base64.ts +30 -1
  180. 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
+ });