@hot-updater/react-native 0.30.12 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HotUpdater.podspec +1 -0
- package/android/build.gradle +1 -0
- package/android/src/main/java/com/hotupdater/BsdiffPatch.kt +284 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +885 -5
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +13 -2
- package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +37 -8
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +48 -12
- package/android/src/main/java/com/hotupdater/SignatureVerifier.kt +60 -22
- package/android/src/newarch/HotUpdater.kt +14 -2
- package/android/src/newarch/HotUpdaterModule.kt +112 -1
- package/android/src/oldarch/HotUpdater.kt +14 -2
- package/android/src/oldarch/HotUpdaterModule.kt +107 -3
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +46 -0
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +29 -1
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +4 -1
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.h +16 -0
- package/ios/HotUpdater/Internal/BsdiffPatchBridge.mm +343 -0
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +1093 -47
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +2 -0
- package/ios/HotUpdater/Internal/HotUpdater.mm +12 -2
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +64 -5
- package/ios/HotUpdater/Internal/NotificationExtension.swift +2 -1
- package/ios/HotUpdater/Internal/SignatureVerifier.swift +28 -5
- package/ios/HotUpdater/Internal/StreamingTarArchiveExtractor.swift +11 -0
- package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +11 -5
- package/lib/commonjs/DefaultResolver.js +6 -2
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/DefaultResolver.spec.js +79 -3
- package/lib/commonjs/DefaultResolver.spec.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +6 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +53 -18
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.spec.js +117 -1
- package/lib/commonjs/index.spec.js.map +1 -1
- package/lib/commonjs/native.js +40 -16
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +119 -3
- package/lib/commonjs/native.spec.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +8 -0
- package/lib/commonjs/sdkVersion.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/store.js +72 -5
- package/lib/commonjs/store.js.map +1 -1
- package/lib/commonjs/store.spec.js +265 -8
- package/lib/commonjs/store.spec.js.map +1 -1
- package/lib/commonjs/wrap.js +19 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/commonjs/wrap.spec.js +74 -0
- package/lib/commonjs/wrap.spec.js.map +1 -1
- package/lib/module/DefaultResolver.js +6 -2
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/DefaultResolver.spec.js +78 -3
- package/lib/module/DefaultResolver.spec.js.map +1 -1
- package/lib/module/checkForUpdate.js +6 -0
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +53 -18
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.spec.js +117 -1
- package/lib/module/index.spec.js.map +1 -1
- package/lib/module/native.js +40 -16
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +119 -3
- package/lib/module/native.spec.js.map +1 -1
- package/lib/module/sdkVersion.js +4 -0
- package/lib/module/sdkVersion.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/store.js +72 -5
- package/lib/module/store.js.map +1 -1
- package/lib/module/store.spec.js +265 -8
- package/lib/module/store.spec.js.map +1 -1
- package/lib/module/wrap.js +19 -5
- package/lib/module/wrap.js.map +1 -1
- package/lib/module/wrap.spec.js +74 -0
- package/lib/module/wrap.spec.js.map +1 -1
- package/lib/typescript/commonjs/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts +2 -2
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +36 -8
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +56 -3
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +2 -0
- package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/store.d.ts +5 -0
- package/lib/typescript/commonjs/store.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +43 -29
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/DefaultResolver.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts +2 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +36 -8
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +56 -3
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +2 -0
- package/lib/typescript/module/sdkVersion.d.ts.map +1 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +27 -0
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/store.d.ts +5 -0
- package/lib/typescript/module/store.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +43 -29
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +17 -9
- package/src/DefaultResolver.spec.ts +103 -3
- package/src/DefaultResolver.ts +9 -2
- package/src/checkForUpdate.ts +9 -2
- package/src/index.spec.ts +149 -1
- package/src/index.ts +99 -40
- package/src/native.spec.ts +139 -1
- package/src/native.ts +145 -31
- package/src/sdkVersion.ts +1 -0
- package/src/specs/NativeHotUpdater.ts +28 -0
- package/src/store.spec.ts +324 -9
- package/src/store.ts +133 -7
- package/src/wrap.spec.ts +91 -0
- package/src/wrap.tsx +79 -42
|
@@ -2,6 +2,21 @@ import type { TurboModule } from "react-native";
|
|
|
2
2
|
import { TurboModuleRegistry } from "react-native";
|
|
3
3
|
import type { UnsafeObject } from "react-native/Libraries/Types/CodegenTypes";
|
|
4
4
|
|
|
5
|
+
export interface ChangedAsset {
|
|
6
|
+
file?: {
|
|
7
|
+
compression?: "br" | null;
|
|
8
|
+
url: string;
|
|
9
|
+
} | null;
|
|
10
|
+
fileHash: string;
|
|
11
|
+
patch?: {
|
|
12
|
+
algorithm: "bsdiff";
|
|
13
|
+
baseBundleId: string;
|
|
14
|
+
baseFileHash: string;
|
|
15
|
+
patchFileHash: string;
|
|
16
|
+
patchUrl: string;
|
|
17
|
+
} | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
5
20
|
export interface UpdateBundleParams {
|
|
6
21
|
bundleId: string;
|
|
7
22
|
channel?: string;
|
|
@@ -16,6 +31,19 @@ export interface UpdateBundleParams {
|
|
|
16
31
|
* Native determines verification mode by checking for "sig:" prefix.
|
|
17
32
|
*/
|
|
18
33
|
fileHash: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Optional signed manifest URL for manifest-driven installation.
|
|
36
|
+
*/
|
|
37
|
+
manifestUrl?: string | null;
|
|
38
|
+
/**
|
|
39
|
+
* File hash/signature for the manifest file itself.
|
|
40
|
+
*/
|
|
41
|
+
manifestFileHash?: string | null;
|
|
42
|
+
/**
|
|
43
|
+
* Per-file URLs for assets that must be downloaded instead of reused from
|
|
44
|
+
* the currently active bundle.
|
|
45
|
+
*/
|
|
46
|
+
changedAssets?: UnsafeObject | null;
|
|
19
47
|
}
|
|
20
48
|
|
|
21
49
|
export interface Spec extends TurboModule {
|
package/src/store.spec.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const listeners = vi.hoisted(
|
|
4
|
+
() => new Map<string, (event: Record<string, unknown>) => void>(),
|
|
5
|
+
);
|
|
4
6
|
|
|
5
7
|
vi.mock("./native", () => ({
|
|
6
|
-
addListener:
|
|
8
|
+
addListener: vi.fn(
|
|
9
|
+
(eventName: string, listener: (event: Record<string, unknown>) => void) => {
|
|
10
|
+
listeners.set(eventName, listener);
|
|
11
|
+
return () => {
|
|
12
|
+
listeners.delete(eventName);
|
|
13
|
+
};
|
|
14
|
+
},
|
|
15
|
+
),
|
|
7
16
|
}));
|
|
8
17
|
|
|
9
18
|
const importStore = async () => {
|
|
@@ -13,8 +22,8 @@ const importStore = async () => {
|
|
|
13
22
|
|
|
14
23
|
describe("hotUpdaterStore", () => {
|
|
15
24
|
beforeEach(() => {
|
|
25
|
+
listeners.clear();
|
|
16
26
|
vi.resetModules();
|
|
17
|
-
addListenerMock.mockReset();
|
|
18
27
|
});
|
|
19
28
|
|
|
20
29
|
it("does not notify subscribers when state values are unchanged", async () => {
|
|
@@ -35,23 +44,329 @@ describe("hotUpdaterStore", () => {
|
|
|
35
44
|
it("notifies subscribers only when progress events change the snapshot", async () => {
|
|
36
45
|
const store = await importStore();
|
|
37
46
|
const listener = vi.fn();
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
const emitProgress = listeners.get("onProgress");
|
|
48
|
+
|
|
49
|
+
expect(emitProgress).toBeTypeOf("function");
|
|
41
50
|
|
|
42
51
|
const unsubscribe = store.subscribe(listener);
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
emitProgress?.({
|
|
54
|
+
artifactType: "archive",
|
|
55
|
+
progress: 0.5,
|
|
56
|
+
});
|
|
57
|
+
emitProgress?.({
|
|
58
|
+
artifactType: "archive",
|
|
59
|
+
progress: 0.5,
|
|
60
|
+
});
|
|
61
|
+
emitProgress?.({
|
|
62
|
+
artifactType: "archive",
|
|
63
|
+
progress: 1,
|
|
64
|
+
});
|
|
47
65
|
store.setState({ isUpdateDownloaded: true });
|
|
48
66
|
|
|
49
67
|
expect(listener).toHaveBeenCalledTimes(2);
|
|
50
68
|
expect(store.getSnapshot()).toEqual({
|
|
69
|
+
artifactType: "archive",
|
|
70
|
+
details: null,
|
|
71
|
+
downloadedBytes: undefined,
|
|
51
72
|
isUpdateDownloaded: true,
|
|
52
73
|
progress: 1,
|
|
74
|
+
totalBytes: undefined,
|
|
53
75
|
});
|
|
54
76
|
|
|
55
77
|
unsubscribe();
|
|
56
78
|
});
|
|
79
|
+
|
|
80
|
+
it("stores archive byte metadata and notifies when only bytes change", async () => {
|
|
81
|
+
const store = await importStore();
|
|
82
|
+
const listener = vi.fn();
|
|
83
|
+
const emitProgress = listeners.get("onProgress");
|
|
84
|
+
|
|
85
|
+
const unsubscribe = store.subscribe(listener);
|
|
86
|
+
|
|
87
|
+
emitProgress?.({
|
|
88
|
+
artifactType: "archive",
|
|
89
|
+
downloadedBytes: 100.4,
|
|
90
|
+
progress: 0.5,
|
|
91
|
+
totalBytes: 1_000,
|
|
92
|
+
});
|
|
93
|
+
emitProgress?.({
|
|
94
|
+
artifactType: "archive",
|
|
95
|
+
downloadedBytes: 250,
|
|
96
|
+
progress: 0.5,
|
|
97
|
+
totalBytes: 1_000,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
101
|
+
expect(store.getSnapshot()).toEqual({
|
|
102
|
+
artifactType: "archive",
|
|
103
|
+
details: null,
|
|
104
|
+
downloadedBytes: 250,
|
|
105
|
+
isUpdateDownloaded: false,
|
|
106
|
+
progress: 0.5,
|
|
107
|
+
totalBytes: 1_000,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
unsubscribe();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("stores manifest progress metadata while keeping overall progress", async () => {
|
|
114
|
+
const store = await importStore();
|
|
115
|
+
const emitProgress = listeners.get("onProgress");
|
|
116
|
+
|
|
117
|
+
expect(emitProgress).toBeTypeOf("function");
|
|
118
|
+
|
|
119
|
+
emitProgress?.({
|
|
120
|
+
artifactType: "diff",
|
|
121
|
+
details: {
|
|
122
|
+
completedFilesCount: 1,
|
|
123
|
+
files: [
|
|
124
|
+
{
|
|
125
|
+
downloadPath: "index.ios.bundle",
|
|
126
|
+
order: 0,
|
|
127
|
+
path: "index.ios.bundle",
|
|
128
|
+
progress: 1,
|
|
129
|
+
status: "downloaded",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
downloadPath: "assets/logo.png",
|
|
133
|
+
order: 1,
|
|
134
|
+
path: "assets/logo.png",
|
|
135
|
+
progress: 0.5,
|
|
136
|
+
status: "downloading",
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
totalFilesCount: 2,
|
|
140
|
+
},
|
|
141
|
+
progress: 0.42,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(store.getSnapshot()).toEqual({
|
|
145
|
+
artifactType: "diff",
|
|
146
|
+
details: {
|
|
147
|
+
completedFilesCount: 1,
|
|
148
|
+
files: [
|
|
149
|
+
{
|
|
150
|
+
downloadPath: "index.ios.bundle",
|
|
151
|
+
order: 0,
|
|
152
|
+
path: "index.ios.bundle",
|
|
153
|
+
progress: 1,
|
|
154
|
+
status: "downloaded",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
downloadPath: "assets/logo.png",
|
|
158
|
+
order: 1,
|
|
159
|
+
path: "assets/logo.png",
|
|
160
|
+
progress: 0.5,
|
|
161
|
+
status: "downloading",
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
totalFilesCount: 2,
|
|
165
|
+
},
|
|
166
|
+
downloadedBytes: undefined,
|
|
167
|
+
isUpdateDownloaded: false,
|
|
168
|
+
progress: 0.42,
|
|
169
|
+
totalBytes: undefined,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("preserves patch download metadata for manifest diff files", async () => {
|
|
174
|
+
const store = await importStore();
|
|
175
|
+
const emitProgress = listeners.get("onProgress");
|
|
176
|
+
|
|
177
|
+
emitProgress?.({
|
|
178
|
+
artifactType: "diff",
|
|
179
|
+
details: {
|
|
180
|
+
completedFilesCount: 0,
|
|
181
|
+
files: [
|
|
182
|
+
{
|
|
183
|
+
downloadPath: "index.ios.bundle.bsdiff",
|
|
184
|
+
order: 0,
|
|
185
|
+
path: "index.ios.bundle",
|
|
186
|
+
progress: 0.4,
|
|
187
|
+
status: "downloading",
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
totalFilesCount: 1,
|
|
191
|
+
},
|
|
192
|
+
progress: 0.32,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(store.getSnapshot().details?.files).toEqual([
|
|
196
|
+
{
|
|
197
|
+
downloadPath: "index.ios.bundle.bsdiff",
|
|
198
|
+
downloadedBytes: undefined,
|
|
199
|
+
order: 0,
|
|
200
|
+
path: "index.ios.bundle",
|
|
201
|
+
progress: 0.4,
|
|
202
|
+
status: "downloading",
|
|
203
|
+
totalBytes: undefined,
|
|
204
|
+
},
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("stores diff snapshot transitions from downloading to downloaded", async () => {
|
|
209
|
+
const store = await importStore();
|
|
210
|
+
const emitProgress = listeners.get("onProgress");
|
|
211
|
+
|
|
212
|
+
emitProgress?.({
|
|
213
|
+
artifactType: "diff",
|
|
214
|
+
details: {
|
|
215
|
+
completedFilesCount: 0,
|
|
216
|
+
files: [
|
|
217
|
+
{
|
|
218
|
+
order: 0,
|
|
219
|
+
path: "index.ios.bundle",
|
|
220
|
+
progress: 0.6,
|
|
221
|
+
status: "downloading",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
order: 1,
|
|
225
|
+
path: "assets/logo.png",
|
|
226
|
+
progress: 0,
|
|
227
|
+
status: "pending",
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
totalFilesCount: 2,
|
|
231
|
+
},
|
|
232
|
+
progress: 0.4,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
emitProgress?.({
|
|
236
|
+
artifactType: "diff",
|
|
237
|
+
details: {
|
|
238
|
+
completedFilesCount: 1,
|
|
239
|
+
files: [
|
|
240
|
+
{
|
|
241
|
+
downloadPath: "index.ios.bundle",
|
|
242
|
+
order: 0,
|
|
243
|
+
path: "index.ios.bundle",
|
|
244
|
+
progress: 1,
|
|
245
|
+
status: "downloaded",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
downloadPath: "assets/logo.png",
|
|
249
|
+
order: 1,
|
|
250
|
+
path: "assets/logo.png",
|
|
251
|
+
progress: 0,
|
|
252
|
+
status: "pending",
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
totalFilesCount: 2,
|
|
256
|
+
},
|
|
257
|
+
progress: 0.6,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(store.getSnapshot()).toEqual({
|
|
261
|
+
artifactType: "diff",
|
|
262
|
+
details: {
|
|
263
|
+
completedFilesCount: 1,
|
|
264
|
+
files: [
|
|
265
|
+
{
|
|
266
|
+
downloadPath: "index.ios.bundle",
|
|
267
|
+
order: 0,
|
|
268
|
+
path: "index.ios.bundle",
|
|
269
|
+
progress: 1,
|
|
270
|
+
status: "downloaded",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
downloadPath: "assets/logo.png",
|
|
274
|
+
order: 1,
|
|
275
|
+
path: "assets/logo.png",
|
|
276
|
+
progress: 0,
|
|
277
|
+
status: "pending",
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
totalFilesCount: 2,
|
|
281
|
+
},
|
|
282
|
+
downloadedBytes: undefined,
|
|
283
|
+
isUpdateDownloaded: false,
|
|
284
|
+
progress: 0.6,
|
|
285
|
+
totalBytes: undefined,
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("clears manifest metadata when archive progress events arrive", async () => {
|
|
290
|
+
const store = await importStore();
|
|
291
|
+
const emitProgress = listeners.get("onProgress");
|
|
292
|
+
|
|
293
|
+
emitProgress?.({
|
|
294
|
+
artifactType: "diff",
|
|
295
|
+
details: {
|
|
296
|
+
completedFilesCount: 2,
|
|
297
|
+
files: [
|
|
298
|
+
{
|
|
299
|
+
order: 0,
|
|
300
|
+
path: "index.ios.bundle",
|
|
301
|
+
progress: 1,
|
|
302
|
+
status: "downloaded",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
order: 1,
|
|
306
|
+
path: "assets/logo.png",
|
|
307
|
+
progress: 1,
|
|
308
|
+
status: "downloaded",
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
order: 2,
|
|
312
|
+
path: "assets/bg.png",
|
|
313
|
+
progress: 0,
|
|
314
|
+
status: "pending",
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
totalFilesCount: 3,
|
|
318
|
+
},
|
|
319
|
+
progress: 0.9,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
emitProgress?.({
|
|
323
|
+
artifactType: "archive",
|
|
324
|
+
progress: 0.25,
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(store.getSnapshot()).toEqual({
|
|
328
|
+
artifactType: "archive",
|
|
329
|
+
details: null,
|
|
330
|
+
downloadedBytes: undefined,
|
|
331
|
+
isUpdateDownloaded: false,
|
|
332
|
+
progress: 0.25,
|
|
333
|
+
totalBytes: undefined,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("clears stale manifest metadata even when archive events omit optional fields", async () => {
|
|
338
|
+
const store = await importStore();
|
|
339
|
+
const emitProgress = listeners.get("onProgress");
|
|
340
|
+
|
|
341
|
+
emitProgress?.({
|
|
342
|
+
artifactType: "diff",
|
|
343
|
+
details: {
|
|
344
|
+
completedFilesCount: 0,
|
|
345
|
+
files: [
|
|
346
|
+
{
|
|
347
|
+
order: 0,
|
|
348
|
+
path: "manifest.asset.png",
|
|
349
|
+
progress: 0,
|
|
350
|
+
status: "failed",
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
totalFilesCount: 1,
|
|
354
|
+
},
|
|
355
|
+
progress: 0.2,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
emitProgress?.({
|
|
359
|
+
artifactType: "archive",
|
|
360
|
+
progress: 0.4,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
expect(store.getSnapshot()).toEqual({
|
|
364
|
+
artifactType: "archive",
|
|
365
|
+
details: null,
|
|
366
|
+
downloadedBytes: undefined,
|
|
367
|
+
isUpdateDownloaded: false,
|
|
368
|
+
progress: 0.4,
|
|
369
|
+
totalBytes: undefined,
|
|
370
|
+
});
|
|
371
|
+
});
|
|
57
372
|
});
|
package/src/store.ts
CHANGED
|
@@ -1,18 +1,88 @@
|
|
|
1
1
|
import useSyncExternalStoreExports from "use-sync-external-store/shim/with-selector";
|
|
2
2
|
|
|
3
|
+
import type {
|
|
4
|
+
HotUpdaterDiffFileSnapshot,
|
|
5
|
+
HotUpdaterDiffProgressDetails,
|
|
6
|
+
HotUpdaterProgressEvent,
|
|
7
|
+
} from "./native";
|
|
3
8
|
import { addListener } from "./native";
|
|
4
9
|
|
|
5
10
|
export type HotUpdaterState = {
|
|
6
11
|
progress: number;
|
|
12
|
+
downloadedBytes: number | undefined;
|
|
13
|
+
totalBytes: number | undefined;
|
|
7
14
|
isUpdateDownloaded: boolean;
|
|
15
|
+
artifactType: "archive" | "diff" | null;
|
|
16
|
+
details: HotUpdaterDiffProgressDetails | null;
|
|
8
17
|
};
|
|
9
18
|
|
|
10
19
|
const { useSyncExternalStoreWithSelector } = useSyncExternalStoreExports;
|
|
11
20
|
|
|
21
|
+
const areDiffDetailsEqual = (
|
|
22
|
+
left: HotUpdaterDiffProgressDetails | null,
|
|
23
|
+
right: HotUpdaterDiffProgressDetails | null,
|
|
24
|
+
) => {
|
|
25
|
+
if (left === right) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (left === null || right === null) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
left.totalFilesCount !== right.totalFilesCount ||
|
|
35
|
+
left.completedFilesCount !== right.completedFilesCount ||
|
|
36
|
+
left.files.length !== right.files.length
|
|
37
|
+
) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return left.files.every((leftFile, index) => {
|
|
42
|
+
const rightFile = right.files[index];
|
|
43
|
+
return (
|
|
44
|
+
rightFile !== undefined &&
|
|
45
|
+
leftFile.order === rightFile.order &&
|
|
46
|
+
leftFile.path === rightFile.path &&
|
|
47
|
+
leftFile.downloadPath === rightFile.downloadPath &&
|
|
48
|
+
leftFile.downloadedBytes === rightFile.downloadedBytes &&
|
|
49
|
+
leftFile.progress === rightFile.progress &&
|
|
50
|
+
leftFile.status === rightFile.status &&
|
|
51
|
+
leftFile.totalBytes === rightFile.totalBytes
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const areStatesEqual = (left: HotUpdaterState, right: HotUpdaterState) => {
|
|
57
|
+
return (
|
|
58
|
+
left.progress === right.progress &&
|
|
59
|
+
left.downloadedBytes === right.downloadedBytes &&
|
|
60
|
+
left.totalBytes === right.totalBytes &&
|
|
61
|
+
left.isUpdateDownloaded === right.isUpdateDownloaded &&
|
|
62
|
+
left.artifactType === right.artifactType &&
|
|
63
|
+
areDiffDetailsEqual(left.details, right.details)
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const normalizeByteCount = (value: number | undefined) => {
|
|
68
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
return Math.round(value);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const normalizeDownloadPath = (value: string | undefined) => {
|
|
75
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
76
|
+
};
|
|
77
|
+
|
|
12
78
|
const createHotUpdaterStore = () => {
|
|
13
79
|
let state: HotUpdaterState = {
|
|
14
80
|
progress: 0,
|
|
81
|
+
downloadedBytes: undefined,
|
|
82
|
+
totalBytes: undefined,
|
|
15
83
|
isUpdateDownloaded: false,
|
|
84
|
+
artifactType: null,
|
|
85
|
+
details: null,
|
|
16
86
|
};
|
|
17
87
|
|
|
18
88
|
const getSnapshot = () => {
|
|
@@ -27,8 +97,39 @@ const createHotUpdaterStore = () => {
|
|
|
27
97
|
}
|
|
28
98
|
};
|
|
29
99
|
|
|
100
|
+
const normalizeDiffDetails = (
|
|
101
|
+
details: HotUpdaterDiffProgressDetails,
|
|
102
|
+
): HotUpdaterDiffProgressDetails => {
|
|
103
|
+
const totalFilesCount = Math.max(0, details.totalFilesCount);
|
|
104
|
+
const normalizedFiles: HotUpdaterDiffFileSnapshot[] = details.files
|
|
105
|
+
.map((file) => {
|
|
106
|
+
const downloadPath =
|
|
107
|
+
normalizeDownloadPath(file.downloadPath) ?? file.path;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
downloadPath,
|
|
111
|
+
downloadedBytes: normalizeByteCount(file.downloadedBytes),
|
|
112
|
+
order: Math.max(0, file.order),
|
|
113
|
+
path: file.path,
|
|
114
|
+
progress: Math.max(0, Math.min(file.progress, 1)),
|
|
115
|
+
status: file.status,
|
|
116
|
+
totalBytes: normalizeByteCount(file.totalBytes),
|
|
117
|
+
};
|
|
118
|
+
})
|
|
119
|
+
.sort((left, right) => left.order - right.order);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
completedFilesCount: Math.max(
|
|
123
|
+
0,
|
|
124
|
+
Math.min(details.completedFilesCount, totalFilesCount),
|
|
125
|
+
),
|
|
126
|
+
files: normalizedFiles,
|
|
127
|
+
totalFilesCount,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
30
131
|
const setState = (newState: Partial<HotUpdaterState>) => {
|
|
31
|
-
// Merge first, then normalize derived fields
|
|
132
|
+
// Merge first, then normalize derived fields.
|
|
32
133
|
const nextState: HotUpdaterState = {
|
|
33
134
|
...state,
|
|
34
135
|
...newState,
|
|
@@ -38,7 +139,8 @@ const createHotUpdaterStore = () => {
|
|
|
38
139
|
// If `progress` is not provided but `isUpdateDownloaded` is,
|
|
39
140
|
// honor the explicit value.
|
|
40
141
|
if ("progress" in newState && typeof newState.progress === "number") {
|
|
41
|
-
nextState.
|
|
142
|
+
nextState.progress = Math.max(0, Math.min(newState.progress, 1));
|
|
143
|
+
nextState.isUpdateDownloaded = nextState.progress >= 1;
|
|
42
144
|
} else if (
|
|
43
145
|
"isUpdateDownloaded" in newState &&
|
|
44
146
|
typeof newState.isUpdateDownloaded === "boolean"
|
|
@@ -46,10 +148,15 @@ const createHotUpdaterStore = () => {
|
|
|
46
148
|
nextState.isUpdateDownloaded = newState.isUpdateDownloaded;
|
|
47
149
|
}
|
|
48
150
|
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
151
|
+
if (newState.details === undefined) {
|
|
152
|
+
nextState.details = state.details;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (nextState.artifactType !== "diff") {
|
|
156
|
+
nextState.details = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (areStatesEqual(state, nextState)) {
|
|
53
160
|
return;
|
|
54
161
|
}
|
|
55
162
|
|
|
@@ -57,6 +164,25 @@ const createHotUpdaterStore = () => {
|
|
|
57
164
|
emitChange();
|
|
58
165
|
};
|
|
59
166
|
|
|
167
|
+
const applyProgressEvent = (event: HotUpdaterProgressEvent) => {
|
|
168
|
+
setState({
|
|
169
|
+
artifactType: event.artifactType,
|
|
170
|
+
details:
|
|
171
|
+
event.artifactType === "diff"
|
|
172
|
+
? normalizeDiffDetails(event.details)
|
|
173
|
+
: null,
|
|
174
|
+
downloadedBytes:
|
|
175
|
+
event.artifactType === "archive"
|
|
176
|
+
? normalizeByteCount(event.downloadedBytes)
|
|
177
|
+
: undefined,
|
|
178
|
+
progress: event.progress,
|
|
179
|
+
totalBytes:
|
|
180
|
+
event.artifactType === "archive"
|
|
181
|
+
? normalizeByteCount(event.totalBytes)
|
|
182
|
+
: undefined,
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
|
|
60
186
|
const subscribe = (listener: () => void) => {
|
|
61
187
|
listeners.add(listener);
|
|
62
188
|
return () => listeners.delete(listener);
|
|
@@ -64,7 +190,7 @@ const createHotUpdaterStore = () => {
|
|
|
64
190
|
|
|
65
191
|
// Subscribe to native onProgress events
|
|
66
192
|
// This listener is registered once when the store is created
|
|
67
|
-
addListener("onProgress",
|
|
193
|
+
addListener("onProgress", applyProgressEvent);
|
|
68
194
|
|
|
69
195
|
return { getSnapshot, setState, subscribe };
|
|
70
196
|
};
|
package/src/wrap.spec.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type React from "react";
|
|
1
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
3
|
|
|
4
|
+
import type { HotUpdaterOptions } from "./wrap";
|
|
5
|
+
|
|
3
6
|
const mocks = vi.hoisted(() => ({
|
|
4
7
|
addListener: vi.fn(() => () => {}),
|
|
5
8
|
checkForUpdate: vi.fn(),
|
|
@@ -78,4 +81,92 @@ describe("HotUpdater wrap initialization", () => {
|
|
|
78
81
|
requestTimeout: 1000,
|
|
79
82
|
});
|
|
80
83
|
});
|
|
84
|
+
|
|
85
|
+
it("calls init onError when app-ready notification fails", async () => {
|
|
86
|
+
vi.useFakeTimers();
|
|
87
|
+
|
|
88
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
89
|
+
const error = new Error("notify failed");
|
|
90
|
+
const onError = vi.fn();
|
|
91
|
+
const requestAnimationFrame = vi.fn(
|
|
92
|
+
(callback: (timestamp: number) => void) => {
|
|
93
|
+
setTimeout(() => callback(0), 0);
|
|
94
|
+
return 1;
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
vi.stubGlobal("requestAnimationFrame", requestAnimationFrame);
|
|
98
|
+
const resolver = {
|
|
99
|
+
checkUpdate: vi.fn(),
|
|
100
|
+
notifyAppReady: vi.fn().mockRejectedValue(error),
|
|
101
|
+
};
|
|
102
|
+
const { init } = await import("./wrap");
|
|
103
|
+
|
|
104
|
+
init({
|
|
105
|
+
resolver,
|
|
106
|
+
onError,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await vi.runOnlyPendingTimersAsync();
|
|
110
|
+
|
|
111
|
+
expect(onError).toHaveBeenCalledWith(error);
|
|
112
|
+
warn.mockRestore();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("warns when the deprecated manual wrap HOC is used", async () => {
|
|
116
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
117
|
+
const { wrap } = await import("./wrap");
|
|
118
|
+
|
|
119
|
+
wrap({
|
|
120
|
+
resolver: {
|
|
121
|
+
checkUpdate: vi.fn(),
|
|
122
|
+
notifyAppReady: vi.fn(),
|
|
123
|
+
},
|
|
124
|
+
updateMode: "manual",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(warn).toHaveBeenCalledWith(
|
|
128
|
+
expect.stringContaining(
|
|
129
|
+
'HotUpdater.wrap({ updateMode: "manual" }) is deprecated',
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
warn.mockRestore();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("preserves wrapped component prop inference", async () => {
|
|
136
|
+
const { wrap } = await import("./wrap");
|
|
137
|
+
const Component: React.ComponentType<{ title: string }> = () => null;
|
|
138
|
+
|
|
139
|
+
const WrappedComponent = wrap({
|
|
140
|
+
resolver: {
|
|
141
|
+
checkUpdate: vi.fn(),
|
|
142
|
+
notifyAppReady: vi.fn(),
|
|
143
|
+
},
|
|
144
|
+
updateMode: "auto",
|
|
145
|
+
updateStrategy: "appVersion",
|
|
146
|
+
})(Component);
|
|
147
|
+
|
|
148
|
+
const acceptsTitleProps: React.ComponentType<{ title: string }> =
|
|
149
|
+
WrappedComponent;
|
|
150
|
+
expect(acceptsTitleProps).toBe(WrappedComponent);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("types public wrap options as automatic mode by default", () => {
|
|
154
|
+
const autoOptions = {
|
|
155
|
+
baseURL: "https://updates.example.com",
|
|
156
|
+
updateStrategy: "appVersion",
|
|
157
|
+
} satisfies HotUpdaterOptions;
|
|
158
|
+
const explicitAutoOptions = {
|
|
159
|
+
baseURL: "https://updates.example.com",
|
|
160
|
+
updateMode: "auto",
|
|
161
|
+
updateStrategy: "appVersion",
|
|
162
|
+
} satisfies HotUpdaterOptions;
|
|
163
|
+
const manualOptions = {
|
|
164
|
+
baseURL: "https://updates.example.com",
|
|
165
|
+
updateMode: "manual",
|
|
166
|
+
} satisfies HotUpdaterOptions;
|
|
167
|
+
|
|
168
|
+
expect(autoOptions.updateStrategy).toBe("appVersion");
|
|
169
|
+
expect(explicitAutoOptions.updateMode).toBe("auto");
|
|
170
|
+
expect(manualOptions.updateMode).toBe("manual");
|
|
171
|
+
});
|
|
81
172
|
});
|