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,803 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import noop from "../../../../utils/noop";
|
|
3
|
+
import sleep from "../../../../utils/sleep";
|
|
4
|
+
import TaskCanceller from "../../../../utils/task_canceller";
|
|
5
|
+
import ManifestFetcher from "../manifest_fetcher";
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
mockConfigGetCurrent,
|
|
12
|
+
mockFormatError,
|
|
13
|
+
mockLogInfo,
|
|
14
|
+
mockLogWarn,
|
|
15
|
+
mockLogDebug,
|
|
16
|
+
mockLoadManifest,
|
|
17
|
+
mockParseManifest,
|
|
18
|
+
mockScheduleRequestPromise,
|
|
19
|
+
mockErrorSelector,
|
|
20
|
+
MockManifest,
|
|
21
|
+
} = vi.hoisted(() => {
|
|
22
|
+
class MockManifestImplem {
|
|
23
|
+
expired: Promise<void> | null = null;
|
|
24
|
+
lifetime: number | undefined = undefined;
|
|
25
|
+
clockOffset: number | undefined = undefined;
|
|
26
|
+
updateUrl: string | undefined = undefined;
|
|
27
|
+
replace = vi.fn();
|
|
28
|
+
update = vi.fn();
|
|
29
|
+
getUrls = vi.fn(() => ["http://example.com/manifest"]);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
mockConfigGetCurrent: vi.fn(),
|
|
33
|
+
mockFormatError: vi.fn((e: unknown) => e),
|
|
34
|
+
mockLogInfo: vi.fn(),
|
|
35
|
+
mockLogWarn: vi.fn(),
|
|
36
|
+
mockLogDebug: vi.fn(),
|
|
37
|
+
mockLoadManifest: vi.fn(),
|
|
38
|
+
mockParseManifest: vi.fn(),
|
|
39
|
+
mockScheduleRequestPromise: vi.fn((fn: () => Promise<unknown>) => fn()),
|
|
40
|
+
mockErrorSelector: vi.fn((e: unknown) => e),
|
|
41
|
+
MockManifest: MockManifestImplem,
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
vi.mock("../../../../config", () => ({ default: { getCurrent: mockConfigGetCurrent } }));
|
|
46
|
+
vi.mock("../../../../errors", () => ({ formatError: mockFormatError }));
|
|
47
|
+
vi.mock("../../../../log", () => ({
|
|
48
|
+
default: { info: mockLogInfo, warn: mockLogWarn, debug: mockLogDebug },
|
|
49
|
+
}));
|
|
50
|
+
vi.mock("../../../../manifest/classes", () => ({ default: MockManifest }));
|
|
51
|
+
vi.mock("../../utils/error_selector", () => ({ default: mockErrorSelector }));
|
|
52
|
+
vi.mock("../../utils/schedule_request", () => ({
|
|
53
|
+
scheduleRequestPromise: mockScheduleRequestPromise,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
const DEFAULT_CONFIG = {
|
|
57
|
+
DEFAULT_REQUEST_TIMEOUT: 30_000,
|
|
58
|
+
DEFAULT_CONNECTION_TIMEOUT: 10_000,
|
|
59
|
+
DEFAULT_MAX_MANIFEST_REQUEST_RETRY: 4,
|
|
60
|
+
INITIAL_BACKOFF_DELAY_BASE: { LOW_LATENCY: 100, REGULAR: 200 },
|
|
61
|
+
MAX_BACKOFF_DELAY_BASE: { LOW_LATENCY: 3_000, REGULAR: 10_000 },
|
|
62
|
+
MAX_CONSECUTIVE_MANIFEST_PARSING_IN_UNSAFE_MODE: 5,
|
|
63
|
+
MIN_MANIFEST_PARSING_TIME_TO_ENTER_UNSAFE_MODE: 100,
|
|
64
|
+
FAILED_PARTIAL_UPDATE_MANIFEST_REFRESH_DELAY: 2_000,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function makeSettings(overrides: Record<string, unknown> = {}) {
|
|
68
|
+
return {
|
|
69
|
+
lowLatencyMode: false,
|
|
70
|
+
maxRetry: undefined,
|
|
71
|
+
requestTimeout: undefined,
|
|
72
|
+
connectionTimeout: undefined,
|
|
73
|
+
minimumManifestUpdateInterval: 0,
|
|
74
|
+
initialManifest: undefined,
|
|
75
|
+
cmcdDataBuilder: null,
|
|
76
|
+
...overrides,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makePipelines(): any {
|
|
81
|
+
return {
|
|
82
|
+
transportName: "dash" as const,
|
|
83
|
+
manifest: { loadManifest: mockLoadManifest, parseManifest: mockParseManifest },
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Resolved load response with sensible defaults */
|
|
88
|
+
function makeLoadResponse(overrides: Record<string, unknown> = {}) {
|
|
89
|
+
return {
|
|
90
|
+
responseData: {},
|
|
91
|
+
size: 100,
|
|
92
|
+
requestDuration: 10,
|
|
93
|
+
sendingTime: 1_000,
|
|
94
|
+
receivedTime: 1_010,
|
|
95
|
+
...overrides,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
describe.skip("ManifestFetcher", () => {
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
mockConfigGetCurrent.mockReturnValue(DEFAULT_CONFIG);
|
|
102
|
+
mockFormatError.mockImplementation((e: unknown) => e);
|
|
103
|
+
mockErrorSelector.mockImplementation((e: unknown) => e);
|
|
104
|
+
mockScheduleRequestPromise.mockImplementation((fn: () => Promise<unknown>) => fn());
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
vi.resetAllMocks();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("before start()", () => {
|
|
112
|
+
it("scheduleManualRefresh is a no-op and does not throw", () => {
|
|
113
|
+
const fetcher = new ManifestFetcher(
|
|
114
|
+
["http://example.com/manifest"],
|
|
115
|
+
makePipelines(),
|
|
116
|
+
makeSettings(),
|
|
117
|
+
);
|
|
118
|
+
expect(() =>
|
|
119
|
+
fetcher.scheduleManualRefresh({
|
|
120
|
+
enablePartialRefresh: false,
|
|
121
|
+
delay: 0,
|
|
122
|
+
canUseUnsafeMode: false,
|
|
123
|
+
}),
|
|
124
|
+
).not.toThrow();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("dispose()", () => {
|
|
129
|
+
it("calls cancel with the given reason", () => {
|
|
130
|
+
const mockCancel = vi.spyOn(TaskCanceller.prototype, "cancel");
|
|
131
|
+
const fetcher = new ManifestFetcher(
|
|
132
|
+
["http://example.com/manifest"],
|
|
133
|
+
makePipelines(),
|
|
134
|
+
makeSettings(),
|
|
135
|
+
);
|
|
136
|
+
fetcher.dispose("test reason");
|
|
137
|
+
expect(mockCancel).toHaveBeenCalledWith("test reason");
|
|
138
|
+
mockCancel.mockRestore();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("uses a default message when reason is undefined", () => {
|
|
142
|
+
const mockCancel = vi.spyOn(TaskCanceller.prototype, "cancel");
|
|
143
|
+
const fetcher = new ManifestFetcher(
|
|
144
|
+
["http://example.com/manifest"],
|
|
145
|
+
makePipelines(),
|
|
146
|
+
makeSettings(),
|
|
147
|
+
);
|
|
148
|
+
fetcher.dispose(undefined);
|
|
149
|
+
expect(mockCancel).toHaveBeenCalledWith("ManifestFetcher dispose");
|
|
150
|
+
mockCancel.mockRestore();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("start() with a Manifest instance as initialManifest", () => {
|
|
155
|
+
it("emits manifestReady immediately without fetching", async () => {
|
|
156
|
+
const manifest = new MockManifest();
|
|
157
|
+
const fetcher = new ManifestFetcher(
|
|
158
|
+
undefined,
|
|
159
|
+
makePipelines(),
|
|
160
|
+
makeSettings({ initialManifest: manifest }),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const onReady = vi.fn();
|
|
164
|
+
fetcher.addEventListener("manifestReady", onReady);
|
|
165
|
+
fetcher.start();
|
|
166
|
+
await flushPromises();
|
|
167
|
+
|
|
168
|
+
expect(onReady).toHaveBeenCalledWith(manifest);
|
|
169
|
+
expect(mockLoadManifest).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("calling start() twice only emits manifestReady once", async () => {
|
|
173
|
+
const manifest = new MockManifest();
|
|
174
|
+
const fetcher = new ManifestFetcher(
|
|
175
|
+
undefined,
|
|
176
|
+
makePipelines(),
|
|
177
|
+
makeSettings({ initialManifest: manifest }),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const onReady = vi.fn();
|
|
181
|
+
fetcher.addEventListener("manifestReady", onReady);
|
|
182
|
+
fetcher.start();
|
|
183
|
+
fetcher.start();
|
|
184
|
+
await flushPromises();
|
|
185
|
+
|
|
186
|
+
expect(onReady).toHaveBeenCalledTimes(1);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("start() with raw initialManifest", () => {
|
|
191
|
+
it("parses it and emits manifestReady (sync parser)", async () => {
|
|
192
|
+
const manifest = new MockManifest();
|
|
193
|
+
mockParseManifest.mockReturnValue({ manifest });
|
|
194
|
+
|
|
195
|
+
const fetcher = new ManifestFetcher(
|
|
196
|
+
["http://example.com/manifest"],
|
|
197
|
+
makePipelines(),
|
|
198
|
+
makeSettings({ initialManifest: { raw: true } }),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const onReady = vi.fn();
|
|
202
|
+
fetcher.addEventListener("manifestReady", onReady);
|
|
203
|
+
fetcher.start();
|
|
204
|
+
await flushPromises();
|
|
205
|
+
|
|
206
|
+
expect(mockParseManifest).toHaveBeenCalled();
|
|
207
|
+
expect(onReady).toHaveBeenCalledWith(manifest);
|
|
208
|
+
expect(mockLoadManifest).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("parses it and emits manifestReady (async parser)", async () => {
|
|
212
|
+
const manifest = new MockManifest();
|
|
213
|
+
mockParseManifest.mockReturnValue(Promise.resolve({ manifest }));
|
|
214
|
+
|
|
215
|
+
const fetcher = new ManifestFetcher(
|
|
216
|
+
["http://example.com/manifest"],
|
|
217
|
+
makePipelines(),
|
|
218
|
+
makeSettings({ initialManifest: { raw: true } }),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const onReady = vi.fn();
|
|
222
|
+
fetcher.addEventListener("manifestReady", onReady);
|
|
223
|
+
fetcher.start();
|
|
224
|
+
await flushPromises();
|
|
225
|
+
|
|
226
|
+
expect(onReady).toHaveBeenCalledWith(manifest);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("start() fetching from URL", () => {
|
|
231
|
+
it("calls loadManifest with the first URL and emits manifestReady", async () => {
|
|
232
|
+
const manifest = new MockManifest();
|
|
233
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
234
|
+
mockParseManifest.mockReturnValue({ manifest });
|
|
235
|
+
|
|
236
|
+
const fetcher = new ManifestFetcher(
|
|
237
|
+
["http://example.com/manifest"],
|
|
238
|
+
makePipelines(),
|
|
239
|
+
makeSettings(),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const onReady = vi.fn();
|
|
243
|
+
fetcher.addEventListener("manifestReady", onReady);
|
|
244
|
+
fetcher.start();
|
|
245
|
+
await flushPromises();
|
|
246
|
+
|
|
247
|
+
expect(mockLoadManifest).toHaveBeenCalledWith(
|
|
248
|
+
"http://example.com/manifest",
|
|
249
|
+
expect.objectContaining({ timeout: 30_000, connectionTimeout: 10_000 }),
|
|
250
|
+
expect.anything(),
|
|
251
|
+
);
|
|
252
|
+
expect(onReady).toHaveBeenCalledWith(manifest);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("passes undefined URL when no URLs are provided", async () => {
|
|
256
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
257
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
258
|
+
|
|
259
|
+
const fetcher = new ManifestFetcher(undefined, makePipelines(), makeSettings());
|
|
260
|
+
fetcher.start();
|
|
261
|
+
await flushPromises();
|
|
262
|
+
|
|
263
|
+
expect(mockLoadManifest).toHaveBeenCalledWith(
|
|
264
|
+
undefined,
|
|
265
|
+
expect.anything(),
|
|
266
|
+
expect.anything(),
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("sets timeout/connectionTimeout to undefined when given negative values", async () => {
|
|
271
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
272
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
273
|
+
|
|
274
|
+
const fetcher = new ManifestFetcher(
|
|
275
|
+
["http://example.com/manifest"],
|
|
276
|
+
makePipelines(),
|
|
277
|
+
makeSettings({ requestTimeout: -1, connectionTimeout: -1 }),
|
|
278
|
+
);
|
|
279
|
+
fetcher.start();
|
|
280
|
+
await flushPromises();
|
|
281
|
+
|
|
282
|
+
expect(mockLoadManifest).toHaveBeenCalledWith(
|
|
283
|
+
expect.anything(),
|
|
284
|
+
expect.objectContaining({ timeout: undefined, connectionTimeout: undefined }),
|
|
285
|
+
expect.anything(),
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("uses custom requestTimeout and connectionTimeout when positive", async () => {
|
|
290
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
291
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
292
|
+
|
|
293
|
+
const fetcher = new ManifestFetcher(
|
|
294
|
+
["http://example.com/manifest"],
|
|
295
|
+
makePipelines(),
|
|
296
|
+
makeSettings({ requestTimeout: 5_000, connectionTimeout: 2_000 }),
|
|
297
|
+
);
|
|
298
|
+
fetcher.start();
|
|
299
|
+
await flushPromises();
|
|
300
|
+
|
|
301
|
+
expect(mockLoadManifest).toHaveBeenCalledWith(
|
|
302
|
+
expect.anything(),
|
|
303
|
+
expect.objectContaining({ timeout: 5_000, connectionTimeout: 2_000 }),
|
|
304
|
+
expect.anything(),
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("includes CMCD payload when cmcdDataBuilder is set", async () => {
|
|
309
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
310
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
311
|
+
const cmcdPayload = { sid: "abc" };
|
|
312
|
+
|
|
313
|
+
const fetcher = new ManifestFetcher(
|
|
314
|
+
["http://example.com/manifest"],
|
|
315
|
+
makePipelines(),
|
|
316
|
+
makeSettings({
|
|
317
|
+
cmcdDataBuilder: { getCmcdDataForManifest: vi.fn(() => cmcdPayload) },
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
fetcher.start();
|
|
321
|
+
await flushPromises();
|
|
322
|
+
|
|
323
|
+
expect(mockLoadManifest).toHaveBeenCalledWith(
|
|
324
|
+
expect.anything(),
|
|
325
|
+
expect.objectContaining({ cmcdPayload }),
|
|
326
|
+
expect.anything(),
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("emits error when loadManifest rejects", async () => {
|
|
331
|
+
const loadError = new Error("network failure");
|
|
332
|
+
mockLoadManifest.mockRejectedValue(loadError);
|
|
333
|
+
|
|
334
|
+
const fetcher = new ManifestFetcher(
|
|
335
|
+
["http://example.com/manifest"],
|
|
336
|
+
makePipelines(),
|
|
337
|
+
makeSettings(),
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const onError = vi.fn();
|
|
341
|
+
fetcher.addEventListener("error", onError);
|
|
342
|
+
fetcher.start();
|
|
343
|
+
await flushPromises();
|
|
344
|
+
|
|
345
|
+
expect(onError).toHaveBeenCalledWith(loadError);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("emits error when parseManifest throws", async () => {
|
|
349
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
350
|
+
const parseError = new Error("parse failure");
|
|
351
|
+
mockFormatError.mockReturnValue(parseError);
|
|
352
|
+
mockParseManifest.mockImplementation(() => {
|
|
353
|
+
throw parseError;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const fetcher = new ManifestFetcher(
|
|
357
|
+
["http://example.com/manifest"],
|
|
358
|
+
makePipelines(),
|
|
359
|
+
makeSettings(),
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const onError = vi.fn();
|
|
363
|
+
fetcher.addEventListener("error", onError);
|
|
364
|
+
fetcher.start();
|
|
365
|
+
await flushPromises();
|
|
366
|
+
|
|
367
|
+
expect(onError).toHaveBeenCalledWith(parseError);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("swallows the error when the canceller is already used at the time of failure", async () => {
|
|
371
|
+
const mockIsUsed = vi.spyOn(TaskCanceller.prototype, "isUsed");
|
|
372
|
+
mockIsUsed.mockImplementation(() => true);
|
|
373
|
+
mockLoadManifest.mockRejectedValue(new Error("boom"));
|
|
374
|
+
|
|
375
|
+
const fetcher = new ManifestFetcher(
|
|
376
|
+
["http://example.com/manifest"],
|
|
377
|
+
makePipelines(),
|
|
378
|
+
makeSettings(),
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const onError = vi.fn();
|
|
382
|
+
fetcher.addEventListener("error", onError);
|
|
383
|
+
fetcher.start();
|
|
384
|
+
await flushPromises();
|
|
385
|
+
|
|
386
|
+
expect(onError).not.toHaveBeenCalled();
|
|
387
|
+
mockIsUsed.mockRestore();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe("backoff settings", () => {
|
|
392
|
+
it("passes REGULAR delays in normal mode and uses config maxRetry as default", async () => {
|
|
393
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
394
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
395
|
+
|
|
396
|
+
const fetcher = new ManifestFetcher(
|
|
397
|
+
["http://example.com/manifest"],
|
|
398
|
+
makePipelines(),
|
|
399
|
+
makeSettings({ lowLatencyMode: false }),
|
|
400
|
+
);
|
|
401
|
+
fetcher.start();
|
|
402
|
+
await flushPromises();
|
|
403
|
+
|
|
404
|
+
expect(mockScheduleRequestPromise).toHaveBeenCalledWith(
|
|
405
|
+
expect.any(Function),
|
|
406
|
+
expect.objectContaining({ baseDelay: 200, maxDelay: 10_000, maxRetry: 4 }),
|
|
407
|
+
expect.anything(),
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it("passes LOW_LATENCY delays when lowLatencyMode is true", async () => {
|
|
412
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
413
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
414
|
+
|
|
415
|
+
const fetcher = new ManifestFetcher(
|
|
416
|
+
["http://example.com/manifest"],
|
|
417
|
+
makePipelines(),
|
|
418
|
+
makeSettings({ lowLatencyMode: true }),
|
|
419
|
+
);
|
|
420
|
+
fetcher.start();
|
|
421
|
+
await flushPromises();
|
|
422
|
+
|
|
423
|
+
expect(mockScheduleRequestPromise).toHaveBeenCalledWith(
|
|
424
|
+
expect.any(Function),
|
|
425
|
+
expect.objectContaining({ baseDelay: 100, maxDelay: 3_000 }),
|
|
426
|
+
expect.anything(),
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
it("uses custom maxRetry from settings when provided", async () => {
|
|
431
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
432
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
433
|
+
|
|
434
|
+
const fetcher = new ManifestFetcher(
|
|
435
|
+
["http://example.com/manifest"],
|
|
436
|
+
makePipelines(),
|
|
437
|
+
makeSettings({ maxRetry: 10 }),
|
|
438
|
+
);
|
|
439
|
+
fetcher.start();
|
|
440
|
+
await flushPromises();
|
|
441
|
+
|
|
442
|
+
expect(mockScheduleRequestPromise).toHaveBeenCalledWith(
|
|
443
|
+
expect.any(Function),
|
|
444
|
+
expect.objectContaining({ maxRetry: 10 }),
|
|
445
|
+
expect.anything(),
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
describe("updateContentUrls()", () => {
|
|
451
|
+
it("does not trigger a load when refreshNow is false", async () => {
|
|
452
|
+
const manifest = new MockManifest();
|
|
453
|
+
const fetcher = new ManifestFetcher(
|
|
454
|
+
["http://example.com/manifest"],
|
|
455
|
+
makePipelines(),
|
|
456
|
+
makeSettings({ initialManifest: manifest }),
|
|
457
|
+
);
|
|
458
|
+
fetcher.start();
|
|
459
|
+
await flushPromises();
|
|
460
|
+
|
|
461
|
+
fetcher.updateContentUrls(["http://new.com/manifest"], false);
|
|
462
|
+
await sleep(0);
|
|
463
|
+
await flushPromises();
|
|
464
|
+
|
|
465
|
+
expect(mockLoadManifest).not.toHaveBeenCalled();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("triggers an immediate refresh with the new URL when refreshNow is true", async () => {
|
|
469
|
+
const manifest = new MockManifest();
|
|
470
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
471
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
472
|
+
|
|
473
|
+
const fetcher = new ManifestFetcher(
|
|
474
|
+
["http://example.com/manifest"],
|
|
475
|
+
makePipelines(),
|
|
476
|
+
makeSettings({ initialManifest: manifest }),
|
|
477
|
+
);
|
|
478
|
+
fetcher.start();
|
|
479
|
+
await flushPromises();
|
|
480
|
+
|
|
481
|
+
fetcher.updateContentUrls(["http://new.com/manifest"], true);
|
|
482
|
+
await sleep(0);
|
|
483
|
+
await flushPromises();
|
|
484
|
+
|
|
485
|
+
expect(mockLoadManifest).toHaveBeenCalledWith(
|
|
486
|
+
"http://new.com/manifest",
|
|
487
|
+
expect.anything(),
|
|
488
|
+
expect.anything(),
|
|
489
|
+
);
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
describe("automatic refresh scheduling", () => {
|
|
494
|
+
beforeEach(() => {
|
|
495
|
+
vi.useFakeTimers();
|
|
496
|
+
});
|
|
497
|
+
afterEach(() => {
|
|
498
|
+
vi.useRealTimers();
|
|
499
|
+
});
|
|
500
|
+
it("schedules a refresh after manifest.lifetime seconds", async () => {
|
|
501
|
+
const manifest = new MockManifest();
|
|
502
|
+
manifest.lifetime = 10;
|
|
503
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
504
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
505
|
+
|
|
506
|
+
const fetcher = new ManifestFetcher(
|
|
507
|
+
["http://example.com/manifest"],
|
|
508
|
+
makePipelines(),
|
|
509
|
+
makeSettings({ initialManifest: manifest }),
|
|
510
|
+
);
|
|
511
|
+
fetcher.start();
|
|
512
|
+
await Promise.resolve();
|
|
513
|
+
|
|
514
|
+
vi.advanceTimersByTime(10_000);
|
|
515
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
516
|
+
await flushPromises();
|
|
517
|
+
|
|
518
|
+
expect(mockLoadManifest).toHaveBeenCalledTimes(1);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it("does not schedule a refresh when lifetime is undefined", async () => {
|
|
522
|
+
const manifest = new MockManifest();
|
|
523
|
+
manifest.lifetime = undefined;
|
|
524
|
+
|
|
525
|
+
const fetcher = new ManifestFetcher(
|
|
526
|
+
undefined,
|
|
527
|
+
makePipelines(),
|
|
528
|
+
makeSettings({ initialManifest: manifest }),
|
|
529
|
+
);
|
|
530
|
+
fetcher.start();
|
|
531
|
+
await Promise.resolve();
|
|
532
|
+
|
|
533
|
+
vi.advanceTimersByTime(100_000);
|
|
534
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
535
|
+
await flushPromises();
|
|
536
|
+
|
|
537
|
+
expect(mockLoadManifest).not.toHaveBeenCalled();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("triggers a refresh when manifest.expired resolves", async () => {
|
|
541
|
+
let resolveExpired!: () => void;
|
|
542
|
+
const manifest = new MockManifest();
|
|
543
|
+
manifest.expired = new Promise<void>((res) => {
|
|
544
|
+
resolveExpired = res;
|
|
545
|
+
});
|
|
546
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
547
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
548
|
+
|
|
549
|
+
const fetcher = new ManifestFetcher(
|
|
550
|
+
["http://example.com/manifest"],
|
|
551
|
+
makePipelines(),
|
|
552
|
+
makeSettings({ initialManifest: manifest }),
|
|
553
|
+
);
|
|
554
|
+
fetcher.start();
|
|
555
|
+
await Promise.resolve();
|
|
556
|
+
|
|
557
|
+
resolveExpired();
|
|
558
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
559
|
+
await flushPromises();
|
|
560
|
+
|
|
561
|
+
expect(mockLoadManifest).toHaveBeenCalledTimes(1);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("does not start a second refresh while one is already in-flight", async () => {
|
|
565
|
+
const manifest = new MockManifest();
|
|
566
|
+
// loader never resolves → refresh stays pending
|
|
567
|
+
mockLoadManifest.mockReturnValue(
|
|
568
|
+
new Promise(() => {
|
|
569
|
+
/* pending */
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
const fetcher = new ManifestFetcher(
|
|
574
|
+
["http://example.com/manifest"],
|
|
575
|
+
makePipelines(),
|
|
576
|
+
makeSettings({ initialManifest: manifest }),
|
|
577
|
+
);
|
|
578
|
+
fetcher.start();
|
|
579
|
+
await Promise.resolve();
|
|
580
|
+
|
|
581
|
+
fetcher.scheduleManualRefresh({
|
|
582
|
+
enablePartialRefresh: false,
|
|
583
|
+
delay: 0,
|
|
584
|
+
canUseUnsafeMode: false,
|
|
585
|
+
});
|
|
586
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
587
|
+
await flushPromises();
|
|
588
|
+
|
|
589
|
+
fetcher.scheduleManualRefresh({
|
|
590
|
+
enablePartialRefresh: false,
|
|
591
|
+
delay: 0,
|
|
592
|
+
canUseUnsafeMode: false,
|
|
593
|
+
});
|
|
594
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
595
|
+
await flushPromises();
|
|
596
|
+
|
|
597
|
+
expect(mockLoadManifest).toHaveBeenCalledTimes(1);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("respects minimumManifestUpdateInterval before firing a manual refresh", async () => {
|
|
601
|
+
const manifest = new MockManifest();
|
|
602
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
603
|
+
mockParseManifest.mockReturnValue({ manifest: new MockManifest() });
|
|
604
|
+
|
|
605
|
+
const fetcher = new ManifestFetcher(
|
|
606
|
+
["http://example.com/manifest"],
|
|
607
|
+
makePipelines(),
|
|
608
|
+
makeSettings({ initialManifest: manifest, minimumManifestUpdateInterval: 5_000 }),
|
|
609
|
+
);
|
|
610
|
+
fetcher.start();
|
|
611
|
+
await Promise.resolve();
|
|
612
|
+
|
|
613
|
+
fetcher.scheduleManualRefresh({
|
|
614
|
+
enablePartialRefresh: false,
|
|
615
|
+
delay: 0,
|
|
616
|
+
canUseUnsafeMode: false,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
vi.advanceTimersByTime(4_999);
|
|
620
|
+
expect(mockLoadManifest).not.toHaveBeenCalled();
|
|
621
|
+
|
|
622
|
+
vi.advanceTimersByTime(1);
|
|
623
|
+
expect(mockLoadManifest).toHaveBeenCalledTimes(1);
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
describe("full vs partial refresh", () => {
|
|
628
|
+
beforeEach(() => {
|
|
629
|
+
vi.useFakeTimers();
|
|
630
|
+
});
|
|
631
|
+
afterEach(() => {
|
|
632
|
+
vi.useRealTimers();
|
|
633
|
+
});
|
|
634
|
+
it("calls manifest.replace() on a full refresh", async () => {
|
|
635
|
+
const manifest = new MockManifest();
|
|
636
|
+
const refreshManifest = new MockManifest();
|
|
637
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
638
|
+
mockParseManifest.mockReturnValue({ manifest: refreshManifest });
|
|
639
|
+
|
|
640
|
+
const fetcher = new ManifestFetcher(
|
|
641
|
+
["http://example.com/manifest"],
|
|
642
|
+
makePipelines(),
|
|
643
|
+
makeSettings({ initialManifest: manifest }),
|
|
644
|
+
);
|
|
645
|
+
fetcher.start();
|
|
646
|
+
await Promise.resolve();
|
|
647
|
+
|
|
648
|
+
fetcher.scheduleManualRefresh({
|
|
649
|
+
enablePartialRefresh: false,
|
|
650
|
+
delay: 0,
|
|
651
|
+
canUseUnsafeMode: false,
|
|
652
|
+
});
|
|
653
|
+
vi.advanceTimersByTime(0);
|
|
654
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
655
|
+
await flushPromises();
|
|
656
|
+
|
|
657
|
+
expect(manifest.replace).toHaveBeenCalledWith(refreshManifest);
|
|
658
|
+
expect(manifest.update).not.toHaveBeenCalled();
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it("calls manifest.update() on a partial refresh when updateUrl is set", async () => {
|
|
662
|
+
const manifest = new MockManifest();
|
|
663
|
+
manifest.updateUrl = "http://example.com/update";
|
|
664
|
+
const refreshManifest = new MockManifest();
|
|
665
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
666
|
+
mockParseManifest.mockReturnValue({ manifest: refreshManifest });
|
|
667
|
+
|
|
668
|
+
const fetcher = new ManifestFetcher(
|
|
669
|
+
["http://example.com/manifest"],
|
|
670
|
+
makePipelines(),
|
|
671
|
+
makeSettings({ initialManifest: manifest }),
|
|
672
|
+
);
|
|
673
|
+
fetcher.start();
|
|
674
|
+
await Promise.resolve();
|
|
675
|
+
|
|
676
|
+
fetcher.scheduleManualRefresh({
|
|
677
|
+
enablePartialRefresh: true,
|
|
678
|
+
delay: 0,
|
|
679
|
+
canUseUnsafeMode: false,
|
|
680
|
+
});
|
|
681
|
+
vi.advanceTimersByTime(0);
|
|
682
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
683
|
+
await flushPromises();
|
|
684
|
+
|
|
685
|
+
expect(manifest.replace).not.toHaveBeenCalled();
|
|
686
|
+
expect(manifest.update).toHaveBeenCalledWith(refreshManifest);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("falls back to a full refresh when manifest.update() throws, after a delay", async () => {
|
|
690
|
+
const manifest = new MockManifest();
|
|
691
|
+
manifest.updateUrl = "http://example.com/update";
|
|
692
|
+
manifest.update.mockImplementationOnce(() => {
|
|
693
|
+
throw new Error("conflict");
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const refreshManifest = new MockManifest();
|
|
697
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
698
|
+
mockParseManifest.mockReturnValue({ manifest: refreshManifest });
|
|
699
|
+
|
|
700
|
+
const fetcher = new ManifestFetcher(
|
|
701
|
+
["http://example.com/manifest"],
|
|
702
|
+
makePipelines(),
|
|
703
|
+
makeSettings({ initialManifest: manifest }),
|
|
704
|
+
);
|
|
705
|
+
fetcher.start();
|
|
706
|
+
await Promise.resolve();
|
|
707
|
+
|
|
708
|
+
fetcher.scheduleManualRefresh({
|
|
709
|
+
enablePartialRefresh: true,
|
|
710
|
+
delay: 0,
|
|
711
|
+
canUseUnsafeMode: false,
|
|
712
|
+
});
|
|
713
|
+
vi.advanceTimersByTime(0);
|
|
714
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
715
|
+
await flushPromises();
|
|
716
|
+
|
|
717
|
+
// Fallback full refresh fires after FAILED_PARTIAL_UPDATE_MANIFEST_REFRESH_DELAY
|
|
718
|
+
vi.advanceTimersByTime(DEFAULT_CONFIG.FAILED_PARTIAL_UPDATE_MANIFEST_REFRESH_DELAY);
|
|
719
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
720
|
+
await flushPromises();
|
|
721
|
+
|
|
722
|
+
expect(manifest.replace).toHaveBeenCalledWith(refreshManifest);
|
|
723
|
+
expect(mockLogWarn).toHaveBeenCalledWith(
|
|
724
|
+
"MF",
|
|
725
|
+
expect.stringContaining("Attempt to update Manifest failed"),
|
|
726
|
+
"Re-downloading the Manifest fully",
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it("uses the prioritized URL (from updateContentUrls) and forces a full refresh", async () => {
|
|
731
|
+
const manifest = new MockManifest();
|
|
732
|
+
manifest.updateUrl = "http://example.com/update";
|
|
733
|
+
const refreshManifest = new MockManifest();
|
|
734
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
735
|
+
mockParseManifest.mockReturnValue({ manifest: refreshManifest });
|
|
736
|
+
|
|
737
|
+
const fetcher = new ManifestFetcher(
|
|
738
|
+
["http://example.com/manifest"],
|
|
739
|
+
makePipelines(),
|
|
740
|
+
makeSettings({ initialManifest: manifest }),
|
|
741
|
+
);
|
|
742
|
+
fetcher.start();
|
|
743
|
+
await Promise.resolve();
|
|
744
|
+
|
|
745
|
+
fetcher.updateContentUrls(["http://prioritized.com/manifest"], false);
|
|
746
|
+
fetcher.scheduleManualRefresh({
|
|
747
|
+
enablePartialRefresh: true,
|
|
748
|
+
delay: 0,
|
|
749
|
+
canUseUnsafeMode: false,
|
|
750
|
+
});
|
|
751
|
+
vi.advanceTimersByTime(0);
|
|
752
|
+
await vi.waitFor(noop, { timeout: 0 }); // real delay
|
|
753
|
+
await flushPromises();
|
|
754
|
+
|
|
755
|
+
expect(mockLoadManifest).toHaveBeenCalledWith(
|
|
756
|
+
"http://prioritized.com/manifest",
|
|
757
|
+
expect.anything(),
|
|
758
|
+
expect.anything(),
|
|
759
|
+
);
|
|
760
|
+
expect(manifest.replace).toHaveBeenCalled();
|
|
761
|
+
expect(manifest.update).not.toHaveBeenCalled();
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
describe("warning events from the parser", () => {
|
|
766
|
+
it("emits a warning for each entry reported via the onWarnings callback", async () => {
|
|
767
|
+
mockLoadManifest.mockResolvedValue(makeLoadResponse());
|
|
768
|
+
|
|
769
|
+
let capturedOnWarnings!: (w: Error[]) => void;
|
|
770
|
+
mockParseManifest.mockImplementation(
|
|
771
|
+
(_data: unknown, _opts: unknown, onWarnings: (w: Error[]) => void) => {
|
|
772
|
+
capturedOnWarnings = onWarnings;
|
|
773
|
+
return { manifest: new MockManifest() };
|
|
774
|
+
},
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
const parseWarning = new Error("minor issue");
|
|
778
|
+
mockFormatError.mockReturnValue(parseWarning);
|
|
779
|
+
|
|
780
|
+
const fetcher = new ManifestFetcher(
|
|
781
|
+
["http://example.com/manifest"],
|
|
782
|
+
makePipelines(),
|
|
783
|
+
makeSettings(),
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
const onWarning = vi.fn();
|
|
787
|
+
fetcher.addEventListener("warning", onWarning);
|
|
788
|
+
fetcher.start();
|
|
789
|
+
await flushPromises();
|
|
790
|
+
|
|
791
|
+
capturedOnWarnings([parseWarning]);
|
|
792
|
+
|
|
793
|
+
expect(onWarning).toHaveBeenCalledWith(parseWarning);
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
/** Flush microtask queue */
|
|
799
|
+
async function flushPromises(ticks = 100) {
|
|
800
|
+
for (let i = 0; i < ticks; i++) {
|
|
801
|
+
await Promise.resolve();
|
|
802
|
+
}
|
|
803
|
+
}
|