@hot-updater/react-native 1.0.0-rc.14 → 1.0.0-rc.16
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/android/proguard-rules.pro +2 -2
- package/android/src/main/java/com/hotupdater/BuiltInAssetResolver.kt +286 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +650 -636
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +3 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +0 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +10 -14
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +19 -0
- package/android/src/main/java/com/hotupdater/TarArchiveInputStream.kt +214 -375
- package/android/src/main/java/com/hotupdater/TarBrArchiveExtractor.kt +119 -0
- package/android/src/newarch/HotUpdater.kt +7 -9
- package/android/src/newarch/HotUpdaterModule.kt +40 -20
- package/android/src/oldarch/HotUpdater.kt +7 -9
- package/android/src/oldarch/HotUpdaterModule.kt +40 -20
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +1126 -74
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -5
- package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +206 -0
- package/ios/HotUpdater/Internal/BrotliFileDecompressor.swift +167 -0
- package/ios/HotUpdater/Internal/BuiltInAssetResolver.swift +196 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +773 -1016
- package/ios/HotUpdater/Internal/BundleMetadata.swift +5 -0
- package/ios/HotUpdater/Internal/FileUtilities.swift +64 -0
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +0 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +5 -27
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +47 -31
- package/ios/HotUpdater/Internal/NotificationExtension.swift +0 -2
- package/ios/HotUpdater/Internal/TarArchiveExtractor.swift +182 -311
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +13 -72
- package/lib/commonjs/checkForUpdate.js +7 -10
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/commonjs/error.js +4 -13
- package/lib/commonjs/error.js.map +1 -1
- package/lib/commonjs/fetchJSON.js +9 -2
- package/lib/commonjs/fetchJSON.js.map +1 -1
- package/lib/commonjs/httpClient.js +151 -39
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/httpClient.spec.js +247 -5
- package/lib/commonjs/httpClient.spec.js.map +1 -1
- package/lib/commonjs/index.js +15 -8
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +101 -0
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +7 -8
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +21 -16
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.js +4 -1
- package/lib/commonjs/notifyAppReadyInsights.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.spec.js +59 -0
- package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +3 -10
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.notification.spec.js +12 -4
- package/lib/commonjs/store.notification.spec.js.map +1 -1
- package/lib/commonjs/store.spec.js +2 -136
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/uuidv7.js +41 -0
- package/lib/commonjs/uuidv7.js.map +1 -0
- package/lib/commonjs/uuidv7.spec.js +30 -0
- package/lib/commonjs/uuidv7.spec.js.map +1 -0
- package/lib/commonjs/wrap.js +1 -3
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +7 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +7 -10
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +9 -4
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
- package/lib/module/error.js +4 -13
- package/lib/module/error.js.map +1 -1
- package/lib/module/fetchJSON.js +7 -1
- package/lib/module/fetchJSON.js.map +1 -1
- package/lib/module/httpClient.js +153 -41
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/httpClient.spec.js +248 -6
- package/lib/module/httpClient.spec.js.map +1 -1
- package/lib/module/index.js +15 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +102 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +7 -8
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +21 -16
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.boundary.spec.js +7 -0
- package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.js +5 -2
- package/lib/module/notifyAppReadyInsights.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.spec.js +59 -0
- package/lib/module/notifyAppReadyInsights.spec.js.map +1 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js +55 -1
- package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +3 -10
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.notification.spec.js +12 -4
- package/lib/module/store.notification.spec.js.map +1 -1
- package/lib/module/store.spec.js +2 -136
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/uuidv7.js +36 -0
- package/lib/module/uuidv7.js.map +1 -0
- package/lib/module/uuidv7.spec.js +30 -0
- package/lib/module/uuidv7.spec.js.map +1 -0
- package/lib/module/wrap.js +1 -3
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +7 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +1 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/error.d.ts +4 -13
- package/lib/typescript/commonjs/error.d.ts.map +1 -1
- package/lib/typescript/commonjs/fetchJSON.d.ts +4 -0
- package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +6 -0
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +0 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +1 -6
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +1 -3
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/uuidv7.d.ts +10 -0
- package/lib/typescript/commonjs/uuidv7.d.ts.map +1 -0
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +7 -4
- package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +1 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/error.d.ts +4 -13
- package/lib/typescript/module/error.d.ts.map +1 -1
- package/lib/typescript/module/fetchJSON.d.ts +4 -0
- package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +6 -0
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +0 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +1 -6
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts +1 -1
- package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -35
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +1 -3
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/uuidv7.d.ts +10 -0
- package/lib/typescript/module/uuidv7.d.ts.map +1 -0
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +7 -4
- package/lib/typescript/module/wrap.types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/checkForUpdate.releaseCatalog.spec.ts +10 -4
- package/src/checkForUpdate.ts +5 -11
- package/src/error.ts +4 -14
- package/src/fetchJSON.ts +12 -1
- package/src/httpClient.spec.ts +289 -8
- package/src/httpClient.ts +224 -67
- package/src/index.spec.ts +70 -1
- package/src/index.ts +18 -7
- package/src/native.spec.ts +23 -16
- package/src/native.ts +12 -24
- package/src/notifyAppReadyInsights.boundary.spec.ts +8 -0
- package/src/notifyAppReadyInsights.spec.ts +60 -0
- package/src/notifyAppReadyInsights.transport.spec.ts +66 -0
- package/src/notifyAppReadyInsights.ts +13 -1
- package/src/sdkVersion.ts +1 -1
- package/src/specs/NativeHotUpdater.ts +8 -36
- package/src/store.notification.spec.ts +13 -3
- package/src/store.spec.ts +0 -157
- package/src/store.ts +2 -23
- package/src/uuidv7.spec.ts +25 -0
- package/src/uuidv7.ts +38 -0
- package/src/wrap.spec.ts +8 -0
- package/src/wrap.tsx +0 -2
- package/src/wrap.types.ts +9 -4
- package/android/src/main/java/com/hotupdater/DecompressService.kt +0 -89
- package/android/src/main/java/com/hotupdater/DecompressionStrategy.kt +0 -26
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/TarGzDecompressionStrategy.kt +0 -117
- package/android/src/main/java/com/hotupdater/ZipDecompressionStrategy.kt +0 -192
- package/android/src/test/java/com/hotupdater/TarArchiveInputStreamTest.kt +0 -68
- package/ios/HotUpdater/Internal/ArchiveExtractionUtilities.swift +0 -178
- package/ios/HotUpdater/Internal/DecompressService.swift +0 -112
- package/ios/HotUpdater/Internal/DecompressionStrategy.swift +0 -22
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +0 -370
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +0 -39
- package/ios/HotUpdater/Internal/TarGzDecompressionStrategy.swift +0 -59
- package/ios/HotUpdater/Internal/ZipArchiveExtractor.swift +0 -482
- package/ios/HotUpdater/Internal/ZipDecompressionStrategy.swift +0 -56
package/src/httpClient.spec.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
isUUIDv7,
|
|
3
|
+
type ArtifactInfo,
|
|
4
|
+
type ReleaseCatalog,
|
|
5
|
+
} from "@hot-updater/core";
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
7
|
|
|
4
|
-
import {
|
|
8
|
+
import { FetchJSONResponseError } from "./fetchJSON";
|
|
9
|
+
import { createHttpClient, type InsightsEventParams } from "./httpClient";
|
|
5
10
|
|
|
6
11
|
const mocks = vi.hoisted(() => {
|
|
7
12
|
Reflect.set(globalThis, "HotUpdater", {
|
|
@@ -18,12 +23,28 @@ vi.mock("./releaseCatalogCache", () => ({
|
|
|
18
23
|
}));
|
|
19
24
|
|
|
20
25
|
vi.mock("./fetchJSON", () => ({
|
|
26
|
+
FetchJSONResponseError: class FetchJSONResponseError extends Error {
|
|
27
|
+
constructor(
|
|
28
|
+
readonly status: number,
|
|
29
|
+
statusText: string,
|
|
30
|
+
) {
|
|
31
|
+
super(statusText);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
21
34
|
fetchJSON: mocks.fetchJSON,
|
|
22
35
|
}));
|
|
23
36
|
|
|
24
37
|
const artifact: ArtifactInfo = {
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
artifactProtocolVersion: 1,
|
|
39
|
+
assets: {
|
|
40
|
+
"index.ios.bundle": {
|
|
41
|
+
file: { url: "/storage/index.bundle.br" },
|
|
42
|
+
fileHash: "bundle-hash",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
manifestFileHash: "manifest-hash",
|
|
46
|
+
manifestUrl: "/storage/manifest.json",
|
|
47
|
+
archiveUrl: "/storage/bundle.tar.br",
|
|
27
48
|
};
|
|
28
49
|
|
|
29
50
|
const catalog: ReleaseCatalog = {
|
|
@@ -93,14 +114,24 @@ describe("private HotUpdater HTTP client", () => {
|
|
|
93
114
|
}),
|
|
94
115
|
).resolves.toEqual({
|
|
95
116
|
...artifact,
|
|
96
|
-
|
|
117
|
+
assets: {
|
|
118
|
+
"index.ios.bundle": {
|
|
119
|
+
file: {
|
|
120
|
+
url: "https://first.example.com/hot-updater/storage/index.bundle.br",
|
|
121
|
+
},
|
|
122
|
+
fileHash: "bundle-hash",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
manifestUrl:
|
|
126
|
+
"https://first.example.com/hot-updater/storage/manifest.json",
|
|
127
|
+
archiveUrl: "https://first.example.com/hot-updater/storage/bundle.tar.br",
|
|
97
128
|
});
|
|
98
129
|
|
|
99
130
|
expect(resolveBaseURL).toHaveBeenCalledOnce();
|
|
100
131
|
expect(mocks.fetchJSON).toHaveBeenCalledWith({
|
|
101
132
|
requestHeaders: undefined,
|
|
102
133
|
requestTimeout: undefined,
|
|
103
|
-
url: "https://first.example.com/hot-updater/artifacts/target/from/current",
|
|
134
|
+
url: "https://first.example.com/hot-updater/artifacts/v1/target/from/current",
|
|
104
135
|
});
|
|
105
136
|
|
|
106
137
|
await client.createSession();
|
|
@@ -110,7 +141,12 @@ describe("private HotUpdater HTTP client", () => {
|
|
|
110
141
|
it("rejects non-storage relative artifact URLs", async () => {
|
|
111
142
|
mocks.fetchJSON.mockResolvedValue({
|
|
112
143
|
...artifact,
|
|
113
|
-
|
|
144
|
+
assets: {
|
|
145
|
+
"index.ios.bundle": {
|
|
146
|
+
file: { url: "/private/index.bundle.br" },
|
|
147
|
+
fileHash: "bundle-hash",
|
|
148
|
+
},
|
|
149
|
+
},
|
|
114
150
|
});
|
|
115
151
|
const session = await createHttpClient(
|
|
116
152
|
"https://updates.example.com",
|
|
@@ -124,9 +160,254 @@ describe("private HotUpdater HTTP client", () => {
|
|
|
124
160
|
).rejects.toThrow("client-relative storage paths");
|
|
125
161
|
});
|
|
126
162
|
|
|
163
|
+
it("rejects a legacy artifact response", async () => {
|
|
164
|
+
mocks.fetchJSON.mockResolvedValue({
|
|
165
|
+
fileHash: "archive-hash",
|
|
166
|
+
fileUrl: "/storage/bundle.zip",
|
|
167
|
+
});
|
|
168
|
+
const session = await createHttpClient(
|
|
169
|
+
"https://updates.example.com",
|
|
170
|
+
).createSession();
|
|
171
|
+
|
|
172
|
+
await expect(
|
|
173
|
+
session.resolveArtifact({
|
|
174
|
+
currentBundleId: "current",
|
|
175
|
+
targetBundleId: "target",
|
|
176
|
+
}),
|
|
177
|
+
).rejects.toThrow("does not support artifact protocol 1");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("reports an old server without the v1 endpoint explicitly", async () => {
|
|
181
|
+
mocks.fetchJSON.mockRejectedValue(
|
|
182
|
+
new FetchJSONResponseError(404, "Not Found"),
|
|
183
|
+
);
|
|
184
|
+
const session = await createHttpClient(
|
|
185
|
+
"https://updates.example.com",
|
|
186
|
+
).createSession();
|
|
187
|
+
|
|
188
|
+
await expect(
|
|
189
|
+
session.resolveArtifact({
|
|
190
|
+
currentBundleId: "current",
|
|
191
|
+
targetBundleId: "target",
|
|
192
|
+
}),
|
|
193
|
+
).rejects.toThrow("does not support artifact protocol 1");
|
|
194
|
+
});
|
|
195
|
+
|
|
127
196
|
it("requires a functional baseURL to resolve to a non-empty string", async () => {
|
|
128
197
|
await expect(createHttpClient(() => "").createSession()).rejects.toThrow(
|
|
129
198
|
"baseURL function must return a non-empty string",
|
|
130
199
|
);
|
|
131
200
|
});
|
|
132
201
|
});
|
|
202
|
+
|
|
203
|
+
const unchangedEvent: InsightsEventParams = {
|
|
204
|
+
appVersion: "1.0.0",
|
|
205
|
+
channel: "production",
|
|
206
|
+
cohort: "123",
|
|
207
|
+
fingerprintHash: null,
|
|
208
|
+
fromBundleId: null,
|
|
209
|
+
installId: "install-id",
|
|
210
|
+
platform: "ios",
|
|
211
|
+
toBundleId: "bundle-id",
|
|
212
|
+
type: "UNCHANGED",
|
|
213
|
+
updateStrategy: null,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** Answers each POST /events with the next response, then 204. */
|
|
217
|
+
const stubEventsEndpoint = (...responses: (number | Response | Error)[]) => {
|
|
218
|
+
const fetchMock = vi.fn<typeof fetch>(async () => {
|
|
219
|
+
const next = responses.shift() ?? 204;
|
|
220
|
+
if (next instanceof Error) throw next;
|
|
221
|
+
return typeof next === "number"
|
|
222
|
+
? new Response(null, { status: next })
|
|
223
|
+
: next;
|
|
224
|
+
});
|
|
225
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
226
|
+
const sentEvents = () =>
|
|
227
|
+
fetchMock.mock.calls.map(
|
|
228
|
+
([, request]) =>
|
|
229
|
+
JSON.parse(String(request?.body)) as {
|
|
230
|
+
eventId: string;
|
|
231
|
+
type: string;
|
|
232
|
+
},
|
|
233
|
+
);
|
|
234
|
+
return { fetchMock, sentEvents };
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
describe("Insights event delivery", () => {
|
|
238
|
+
beforeEach(() => {
|
|
239
|
+
vi.useFakeTimers();
|
|
240
|
+
// No jitter: the first retry waits exactly 1 s and the second 2 s.
|
|
241
|
+
vi.spyOn(Math, "random").mockReturnValue(0.5);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
afterEach(() => {
|
|
245
|
+
vi.useRealTimers();
|
|
246
|
+
vi.unstubAllGlobals();
|
|
247
|
+
vi.restoreAllMocks();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const createSession = () =>
|
|
251
|
+
createHttpClient("https://updates.example.com").createSession();
|
|
252
|
+
|
|
253
|
+
it("retries a 503 in the background under the same UUIDv7 eventId", async () => {
|
|
254
|
+
const { fetchMock, sentEvents } = stubEventsEndpoint(503, 204);
|
|
255
|
+
const session = await createSession();
|
|
256
|
+
|
|
257
|
+
// Settles after the failed first attempt, without waiting for the retry.
|
|
258
|
+
await session.sendInsightsEvent(unchangedEvent);
|
|
259
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
260
|
+
|
|
261
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
262
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
263
|
+
const [first, second] = sentEvents();
|
|
264
|
+
expect(isUUIDv7(first?.eventId)).toBe(true);
|
|
265
|
+
expect(second?.eventId).toBe(first?.eventId);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("waits for a 429's Retry-After before the next attempt", async () => {
|
|
269
|
+
const { fetchMock } = stubEventsEndpoint(
|
|
270
|
+
new Response(null, { headers: { "Retry-After": "7" }, status: 429 }),
|
|
271
|
+
);
|
|
272
|
+
const session = await createSession();
|
|
273
|
+
|
|
274
|
+
await session.sendInsightsEvent(unchangedEvent);
|
|
275
|
+
await vi.advanceTimersByTimeAsync(6_999);
|
|
276
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
277
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
278
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("caps a server's Retry-After at 30 seconds", async () => {
|
|
282
|
+
const { fetchMock } = stubEventsEndpoint(
|
|
283
|
+
new Response(null, { headers: { "Retry-After": "3600" }, status: 503 }),
|
|
284
|
+
);
|
|
285
|
+
const session = await createSession();
|
|
286
|
+
|
|
287
|
+
await session.sendInsightsEvent(unchangedEvent);
|
|
288
|
+
await vi.advanceTimersByTimeAsync(29_999);
|
|
289
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
290
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
291
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("does not retry a 400", async () => {
|
|
295
|
+
const { fetchMock } = stubEventsEndpoint(400);
|
|
296
|
+
const session = await createSession();
|
|
297
|
+
|
|
298
|
+
await expect(session.sendInsightsEvent(unchangedEvent)).rejects.toThrow(
|
|
299
|
+
"Expected HTTP 204 from /events, received 400",
|
|
300
|
+
);
|
|
301
|
+
await vi.runAllTimersAsync();
|
|
302
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("stops after three attempts and warns once", async () => {
|
|
306
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
307
|
+
const { fetchMock } = stubEventsEndpoint(
|
|
308
|
+
500,
|
|
309
|
+
new Error("network unavailable"),
|
|
310
|
+
503,
|
|
311
|
+
);
|
|
312
|
+
const session = await createSession();
|
|
313
|
+
|
|
314
|
+
await session.sendInsightsEvent(unchangedEvent);
|
|
315
|
+
await vi.runAllTimersAsync();
|
|
316
|
+
|
|
317
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
318
|
+
expect(warn).toHaveBeenCalledExactlyOnceWith(
|
|
319
|
+
"[HotUpdater] Insights UNCHANGED event was not delivered:",
|
|
320
|
+
expect.objectContaining({
|
|
321
|
+
message: "Expected HTTP 204 from /events, received 503",
|
|
322
|
+
}),
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("gives each attempt its own timeout", async () => {
|
|
327
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
328
|
+
const fetchMock = vi.fn<typeof fetch>(
|
|
329
|
+
(_input, request) =>
|
|
330
|
+
new Promise((_resolve, reject) => {
|
|
331
|
+
request?.signal?.addEventListener("abort", () => {
|
|
332
|
+
reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
|
|
333
|
+
});
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
337
|
+
const session = await createSession();
|
|
338
|
+
|
|
339
|
+
const sent = session.sendInsightsEvent({
|
|
340
|
+
...unchangedEvent,
|
|
341
|
+
requestTimeout: 2_000,
|
|
342
|
+
});
|
|
343
|
+
await vi.advanceTimersByTimeAsync(2_000);
|
|
344
|
+
await sent;
|
|
345
|
+
// The second attempt starts after a 1 s backoff and gets a full 2 s.
|
|
346
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
347
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
348
|
+
const secondSignal = fetchMock.mock.calls[1]?.[1]?.signal;
|
|
349
|
+
await vi.advanceTimersByTimeAsync(1_999);
|
|
350
|
+
expect(secondSignal?.aborted).toBe(false);
|
|
351
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
352
|
+
expect(secondSignal?.aborted).toBe(true);
|
|
353
|
+
await vi.runAllTimersAsync();
|
|
354
|
+
|
|
355
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
356
|
+
expect(warn).toHaveBeenCalledExactlyOnceWith(
|
|
357
|
+
"[HotUpdater] Insights UNCHANGED event was not delivered:",
|
|
358
|
+
expect.objectContaining({ message: "Request timed out" }),
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("sends a later event after an earlier event's retries without holding its caller", async () => {
|
|
363
|
+
const { fetchMock, sentEvents } = stubEventsEndpoint(503);
|
|
364
|
+
const session = await createSession();
|
|
365
|
+
|
|
366
|
+
await session.sendInsightsEvent(unchangedEvent);
|
|
367
|
+
await session.sendInsightsEvent({
|
|
368
|
+
...unchangedEvent,
|
|
369
|
+
fromBundleId: "bundle-id",
|
|
370
|
+
toBundleId: "next-bundle-id",
|
|
371
|
+
type: "UPDATE_DOWNLOADED",
|
|
372
|
+
updateStrategy: "appVersion",
|
|
373
|
+
});
|
|
374
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
375
|
+
|
|
376
|
+
await vi.runAllTimersAsync();
|
|
377
|
+
const [first, retry, later] = sentEvents();
|
|
378
|
+
expect([first?.type, retry?.type, later?.type]).toEqual([
|
|
379
|
+
"UNCHANGED",
|
|
380
|
+
"UNCHANGED",
|
|
381
|
+
"UPDATE_DOWNLOADED",
|
|
382
|
+
]);
|
|
383
|
+
expect(retry?.eventId).toBe(first?.eventId);
|
|
384
|
+
expect(later?.eventId).not.toBe(first?.eventId);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("keeps sending events after one fails outside its requests", async () => {
|
|
388
|
+
vi.spyOn(console, "warn").mockImplementation(() => {
|
|
389
|
+
throw new Error("console unavailable");
|
|
390
|
+
});
|
|
391
|
+
const { fetchMock, sentEvents } = stubEventsEndpoint(500, 500, 500);
|
|
392
|
+
const session = await createSession();
|
|
393
|
+
|
|
394
|
+
await session.sendInsightsEvent(unchangedEvent);
|
|
395
|
+
await vi.runAllTimersAsync();
|
|
396
|
+
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
397
|
+
|
|
398
|
+
// A queue left rejected would never send this event or settle its caller.
|
|
399
|
+
await session.sendInsightsEvent({
|
|
400
|
+
...unchangedEvent,
|
|
401
|
+
fromBundleId: "bundle-id",
|
|
402
|
+
toBundleId: "next-bundle-id",
|
|
403
|
+
type: "UPDATE_DOWNLOADED",
|
|
404
|
+
updateStrategy: "appVersion",
|
|
405
|
+
});
|
|
406
|
+
expect(sentEvents().map(({ type }) => type)).toEqual([
|
|
407
|
+
"UNCHANGED",
|
|
408
|
+
"UNCHANGED",
|
|
409
|
+
"UNCHANGED",
|
|
410
|
+
"UPDATE_DOWNLOADED",
|
|
411
|
+
]);
|
|
412
|
+
});
|
|
413
|
+
});
|
package/src/httpClient.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ARTIFACT_PROTOCOL_VERSION,
|
|
2
3
|
encodeChannelKey,
|
|
3
4
|
type ArtifactInfo,
|
|
4
5
|
type ReleaseCatalog,
|
|
5
6
|
} from "@hot-updater/core";
|
|
6
7
|
import { canonicalizeAppVersion } from "@hot-updater/plugin-core";
|
|
7
8
|
|
|
8
|
-
import { fetchJSON } from "./fetchJSON";
|
|
9
|
+
import { fetchJSON, FetchJSONResponseError } from "./fetchJSON";
|
|
9
10
|
import { fetchReleaseCatalogWithCache } from "./releaseCatalogCache";
|
|
10
11
|
import { HOT_UPDATER_SDK_VERSION } from "./sdkVersion";
|
|
11
12
|
import type { HotUpdaterBaseURL } from "./types";
|
|
13
|
+
import { createUUIDv7 } from "./uuidv7";
|
|
12
14
|
|
|
13
15
|
export interface ReleaseCatalogRequest {
|
|
14
16
|
readonly platform: "ios" | "android";
|
|
@@ -65,6 +67,12 @@ export interface HotUpdaterHttpSession {
|
|
|
65
67
|
params: ReleaseCatalogRequest,
|
|
66
68
|
) => Promise<ReleaseCatalog>;
|
|
67
69
|
resolveArtifact: (params: ArtifactRequest) => Promise<ArtifactInfo>;
|
|
70
|
+
/**
|
|
71
|
+
* Posts an Insights event under a client `eventId`, retrying a network
|
|
72
|
+
* error, timeout, 429 or 5xx for up to three attempts in all. Resolves once
|
|
73
|
+
* the event is delivered or left to background retries; rejects when its
|
|
74
|
+
* first attempt fails for good, such as with a 400.
|
|
75
|
+
*/
|
|
68
76
|
sendInsightsEvent: (params: InsightsEventParams) => Promise<void>;
|
|
69
77
|
}
|
|
70
78
|
|
|
@@ -101,56 +109,101 @@ const resolveArtifactUrls = (
|
|
|
101
109
|
info: ArtifactInfo,
|
|
102
110
|
): ArtifactInfo => ({
|
|
103
111
|
...info,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
...asset.file,
|
|
129
|
-
url: resolveArtifactUrl(baseURL, asset.file.url),
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
: {}),
|
|
133
|
-
...(asset.patch
|
|
134
|
-
? {
|
|
135
|
-
patch: {
|
|
136
|
-
...asset.patch,
|
|
137
|
-
patchUrl: resolveArtifactUrl(
|
|
138
|
-
baseURL,
|
|
139
|
-
asset.patch.patchUrl,
|
|
140
|
-
),
|
|
141
|
-
},
|
|
142
|
-
}
|
|
143
|
-
: {}),
|
|
144
|
-
},
|
|
145
|
-
]),
|
|
146
|
-
),
|
|
147
|
-
}),
|
|
112
|
+
manifestUrl: resolveArtifactUrl(baseURL, info.manifestUrl),
|
|
113
|
+
...(info.archiveUrl
|
|
114
|
+
? { archiveUrl: resolveArtifactUrl(baseURL, info.archiveUrl) }
|
|
115
|
+
: {}),
|
|
116
|
+
assets: Object.fromEntries(
|
|
117
|
+
Object.entries(info.assets).map(([path, asset]) => [
|
|
118
|
+
path,
|
|
119
|
+
{
|
|
120
|
+
...asset,
|
|
121
|
+
file: {
|
|
122
|
+
...asset.file,
|
|
123
|
+
url: resolveArtifactUrl(baseURL, asset.file.url),
|
|
124
|
+
},
|
|
125
|
+
...(asset.patch
|
|
126
|
+
? {
|
|
127
|
+
patch: {
|
|
128
|
+
...asset.patch,
|
|
129
|
+
patchUrl: resolveArtifactUrl(baseURL, asset.patch.patchUrl),
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
: {}),
|
|
133
|
+
},
|
|
134
|
+
]),
|
|
135
|
+
),
|
|
148
136
|
});
|
|
149
137
|
|
|
150
|
-
const
|
|
138
|
+
const requireArtifactProtocolV1 = (info: ArtifactInfo): ArtifactInfo => {
|
|
139
|
+
if (
|
|
140
|
+
info.artifactProtocolVersion !== ARTIFACT_PROTOCOL_VERSION ||
|
|
141
|
+
!info.assets ||
|
|
142
|
+
!info.manifestUrl ||
|
|
143
|
+
!info.manifestFileHash ||
|
|
144
|
+
(info.archiveUrl !== undefined && typeof info.archiveUrl !== "string")
|
|
145
|
+
) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Server does not support artifact protocol ${ARTIFACT_PROTOCOL_VERSION}.`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
for (const asset of Object.values(info.assets)) {
|
|
151
|
+
if (!asset.file?.url || !asset.fileHash) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
"Artifact protocol 1 requires an original file for every asset.",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return info;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** Attempts per Insights event, the first one included. */
|
|
161
|
+
const INSIGHTS_MAX_ATTEMPTS = 3;
|
|
162
|
+
/** Wait before the second attempt; it doubles for each later attempt. */
|
|
163
|
+
const INSIGHTS_RETRY_BASE_DELAY_MS = 1000;
|
|
164
|
+
/** Longest wait between attempts, a server's `Retry-After` included. */
|
|
165
|
+
const INSIGHTS_RETRY_MAX_DELAY_MS = 30000;
|
|
166
|
+
|
|
167
|
+
type InsightsAttemptFailure = {
|
|
168
|
+
readonly error: Error;
|
|
169
|
+
/** A network error, timeout, 429 or 5xx may pass on a later attempt. */
|
|
170
|
+
readonly retryable: boolean;
|
|
171
|
+
readonly retryAfterMs: number | null;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Reads `Retry-After` as delay-seconds, the form a server under load sends;
|
|
176
|
+
* an HTTP-date falls back to the backoff.
|
|
177
|
+
*/
|
|
178
|
+
const parseRetryAfterMs = (value: string | null): number | null => {
|
|
179
|
+
const seconds = value?.trim();
|
|
180
|
+
return seconds && /^\d+$/.test(seconds) ? Number(seconds) * 1000 : null;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const getRetryDelayMs = (
|
|
184
|
+
failedAttempt: number,
|
|
185
|
+
retryAfterMs: number | null,
|
|
186
|
+
): number =>
|
|
187
|
+
Math.min(
|
|
188
|
+
retryAfterMs ??
|
|
189
|
+
// Jitter spreads the retries of installations that failed together.
|
|
190
|
+
INSIGHTS_RETRY_BASE_DELAY_MS *
|
|
191
|
+
2 ** (failedAttempt - 1) *
|
|
192
|
+
(0.5 + Math.random()),
|
|
193
|
+
INSIGHTS_RETRY_MAX_DELAY_MS,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const wait = (ms: number) =>
|
|
197
|
+
new Promise<void>((resolve) => {
|
|
198
|
+
setTimeout(resolve, ms);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
/** Makes one POST, with its own timeout, and settles with its failure. */
|
|
202
|
+
const postInsightsEvent = async (
|
|
151
203
|
baseURL: string,
|
|
152
204
|
params: InsightsEventParams,
|
|
153
|
-
|
|
205
|
+
eventId: string,
|
|
206
|
+
): Promise<InsightsAttemptFailure | null> => {
|
|
154
207
|
const controller = new AbortController();
|
|
155
208
|
const timeoutId = setTimeout(() => {
|
|
156
209
|
controller.abort();
|
|
@@ -162,6 +215,7 @@ const sendInsightsEvent = async (
|
|
|
162
215
|
appVersion: params.appVersion,
|
|
163
216
|
channel: params.channel,
|
|
164
217
|
cohort: params.cohort,
|
|
218
|
+
eventId,
|
|
165
219
|
fingerprintHash: params.fingerprintHash,
|
|
166
220
|
fromBundleId: params.fromBundleId,
|
|
167
221
|
...(params.fromReleaseId === undefined
|
|
@@ -187,22 +241,109 @@ const sendInsightsEvent = async (
|
|
|
187
241
|
signal: controller.signal,
|
|
188
242
|
});
|
|
189
243
|
|
|
190
|
-
if (response.status
|
|
191
|
-
|
|
244
|
+
if (response.status === 204) return null;
|
|
245
|
+
return {
|
|
246
|
+
error: new Error(
|
|
192
247
|
`Expected HTTP 204 from /events, received ${response.status}`,
|
|
193
|
-
)
|
|
194
|
-
|
|
248
|
+
),
|
|
249
|
+
retryable: response.status === 429 || response.status >= 500,
|
|
250
|
+
retryAfterMs: parseRetryAfterMs(response.headers.get("Retry-After")),
|
|
251
|
+
};
|
|
195
252
|
} catch (error: unknown) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
253
|
+
// fetch rejects only when no response arrived (a timeout or a network
|
|
254
|
+
// error), so a later attempt may still get through.
|
|
255
|
+
return {
|
|
256
|
+
error:
|
|
257
|
+
error instanceof Error && error.name === "AbortError"
|
|
258
|
+
? new Error("Request timed out")
|
|
259
|
+
: error instanceof Error
|
|
260
|
+
? error
|
|
261
|
+
: new Error(String(error)),
|
|
262
|
+
retryable: true,
|
|
263
|
+
retryAfterMs: null,
|
|
264
|
+
};
|
|
200
265
|
} finally {
|
|
201
266
|
clearTimeout(timeoutId);
|
|
202
267
|
}
|
|
203
268
|
};
|
|
204
269
|
|
|
205
|
-
|
|
270
|
+
type InsightsEventSender = (
|
|
271
|
+
baseURL: string,
|
|
272
|
+
params: InsightsEventParams,
|
|
273
|
+
) => Promise<void>;
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Sends Insights events one at a time and retries each in the background.
|
|
277
|
+
*
|
|
278
|
+
* The server keeps an installation's latest report by arrival, so a later
|
|
279
|
+
* event waits behind an earlier one's retries; otherwise a retried
|
|
280
|
+
* UPDATE_APPLIED could land after the UPDATE_DOWNLOADED that followed it.
|
|
281
|
+
* Callers still wait only for first attempts, as they did before retries, so
|
|
282
|
+
* startup, readiness and `updateBundle()` never wait out a backoff: once an
|
|
283
|
+
* event backs off, every waiting caller is released, and a failure after that
|
|
284
|
+
* only warns.
|
|
285
|
+
*/
|
|
286
|
+
const createInsightsEventSender = (): InsightsEventSender => {
|
|
287
|
+
let queue = Promise.resolve();
|
|
288
|
+
let retrying = false;
|
|
289
|
+
const waiting = new Set<() => void>();
|
|
290
|
+
|
|
291
|
+
return (baseURL, params) => {
|
|
292
|
+
// One ID for every attempt, so the server counts a retried POST once.
|
|
293
|
+
const eventId = createUUIDv7();
|
|
294
|
+
|
|
295
|
+
return new Promise<void>((resolve, reject) => {
|
|
296
|
+
let released = false;
|
|
297
|
+
const release = () => {
|
|
298
|
+
released = true;
|
|
299
|
+
waiting.delete(release);
|
|
300
|
+
resolve();
|
|
301
|
+
};
|
|
302
|
+
if (retrying) release();
|
|
303
|
+
else waiting.add(release);
|
|
304
|
+
|
|
305
|
+
queue = queue.then(async () => {
|
|
306
|
+
try {
|
|
307
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
308
|
+
const failure = await postInsightsEvent(baseURL, params, eventId);
|
|
309
|
+
if (failure === null) {
|
|
310
|
+
release();
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
if (!failure.retryable || attempt === INSIGHTS_MAX_ATTEMPTS) {
|
|
314
|
+
waiting.delete(release);
|
|
315
|
+
if (released) {
|
|
316
|
+
console.warn(
|
|
317
|
+
`[HotUpdater] Insights ${params.type} event was not delivered:`,
|
|
318
|
+
failure.error,
|
|
319
|
+
);
|
|
320
|
+
} else {
|
|
321
|
+
reject(failure.error);
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
retrying = true;
|
|
326
|
+
for (const releaseWaiting of waiting) releaseWaiting();
|
|
327
|
+
await wait(getRetryDelayMs(attempt, failure.retryAfterMs));
|
|
328
|
+
}
|
|
329
|
+
} catch (error: unknown) {
|
|
330
|
+
// A failed request settles in postInsightsEvent, so only a bug gets
|
|
331
|
+
// here. A rejected queue would leave every later event, and a caller
|
|
332
|
+
// such as `updateBundle()` waiting on one, pending forever.
|
|
333
|
+
waiting.delete(release);
|
|
334
|
+
if (!released) reject(error);
|
|
335
|
+
} finally {
|
|
336
|
+
retrying = false;
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const createSession = (
|
|
344
|
+
baseURL: string,
|
|
345
|
+
sendInsightsEvent: InsightsEventSender,
|
|
346
|
+
): HotUpdaterHttpSession => ({
|
|
206
347
|
fetchReleaseCatalog: async (params): Promise<ReleaseCatalog> => {
|
|
207
348
|
const channelKey = encodeChannelKey(params.channel);
|
|
208
349
|
let strategyValue: string;
|
|
@@ -241,14 +382,25 @@ const createSession = (baseURL: string): HotUpdaterHttpSession => ({
|
|
|
241
382
|
});
|
|
242
383
|
},
|
|
243
384
|
resolveArtifact: async (params): Promise<ArtifactInfo> => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
params.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
385
|
+
let info: ArtifactInfo;
|
|
386
|
+
try {
|
|
387
|
+
info = await fetchJSON<ArtifactInfo>({
|
|
388
|
+
requestHeaders: params.requestHeaders,
|
|
389
|
+
requestTimeout: params.requestTimeout,
|
|
390
|
+
url: `${baseURL}/artifacts/v1/${encodeURIComponent(
|
|
391
|
+
params.targetBundleId,
|
|
392
|
+
)}/from/${encodeURIComponent(params.currentBundleId)}`,
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (error instanceof FetchJSONResponseError && error.status === 404) {
|
|
396
|
+
throw new Error(
|
|
397
|
+
`Server does not support artifact protocol ${ARTIFACT_PROTOCOL_VERSION}.`,
|
|
398
|
+
{ cause: error },
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
throw error;
|
|
402
|
+
}
|
|
403
|
+
return resolveArtifactUrls(baseURL, requireArtifactProtocolV1(info));
|
|
252
404
|
},
|
|
253
405
|
sendInsightsEvent: (params) => sendInsightsEvent(baseURL, params),
|
|
254
406
|
});
|
|
@@ -256,6 +408,11 @@ const createSession = (baseURL: string): HotUpdaterHttpSession => ({
|
|
|
256
408
|
/** Creates the private HTTP client used by HotUpdater.init and HotUpdater.wrap. */
|
|
257
409
|
export const createHttpClient = (
|
|
258
410
|
baseURL: HotUpdaterBaseURL,
|
|
259
|
-
): HotUpdaterHttpClient =>
|
|
260
|
-
|
|
261
|
-
|
|
411
|
+
): HotUpdaterHttpClient => {
|
|
412
|
+
// Shared by every session, since each report opens its own session.
|
|
413
|
+
const sendInsightsEvent = createInsightsEventSender();
|
|
414
|
+
return {
|
|
415
|
+
createSession: async () =>
|
|
416
|
+
createSession(await resolveBaseURL(baseURL), sendInsightsEvent),
|
|
417
|
+
};
|
|
418
|
+
};
|