react-native-update 10.44.0 → 10.45.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/android/jni/Android.mk +0 -1
- package/android/lib/arm64-v8a/librnupdate.so +0 -0
- package/android/lib/armeabi-v7a/librnupdate.so +0 -0
- package/android/lib/x86/librnupdate.so +0 -0
- package/android/lib/x86_64/librnupdate.so +0 -0
- package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +10 -1
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +6 -2
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -1
- package/cpp/patch_core/patch_core.cpp +16 -1
- package/cpp/patch_core/patch_core.h +6 -0
- package/cpp/patch_core/tests/patch_core_test.cpp +77 -0
- package/harmony/pushy/src/main/cpp/pushy.cpp +1 -93
- package/harmony/pushy/src/main/ets/DownloadTask.ts +13 -10
- package/harmony/pushy/src/main/ets/NativePatchCore.ts +0 -4
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.mm +142 -111
- package/package.json +6 -3
- package/src/client.ts +31 -26
- package/src/context.ts +21 -7
- package/src/core.ts +5 -1
- package/src/index.ts +7 -1
- package/src/provider.tsx +51 -30
- package/src/utils.ts +56 -16
- package/android/jni/DownloadTask.c +0 -56
- package/android/jni/cn_reactnative_modules_update_DownloadTask.h +0 -21
package/android/jni/Android.mk
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -33,7 +33,16 @@ class DownloadTask implements Runnable {
|
|
|
33
33
|
// events on percentage change, so throttle by bytes to avoid flooding the
|
|
34
34
|
// bridge (e.g. a 20MB chunked download would otherwise emit ~5000 events).
|
|
35
35
|
private static final long PROGRESS_BYTES_THRESHOLD = 256 * 1024;
|
|
36
|
-
|
|
36
|
+
// Explicit timeouts: the default client has no call timeout, so a
|
|
37
|
+
// slow-dripping connection could occupy the single-threaded download
|
|
38
|
+
// executor indefinitely and starve queued tasks. The call timeout is a
|
|
39
|
+
// generous upper bound sized for large full-package downloads.
|
|
40
|
+
private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder()
|
|
41
|
+
.connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
|
|
42
|
+
.readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
|
|
43
|
+
.writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
|
|
44
|
+
.callTimeout(10, java.util.concurrent.TimeUnit.MINUTES)
|
|
45
|
+
.build();
|
|
37
46
|
|
|
38
47
|
static {
|
|
39
48
|
NativeUpdateCore.ensureLoaded();
|
|
@@ -214,7 +214,7 @@ public class UpdateContext {
|
|
|
214
214
|
|
|
215
215
|
public void switchVersion(String hash) {
|
|
216
216
|
if (!new File(rootDir, hash+"/index.bundlejs").exists()) {
|
|
217
|
-
throw new
|
|
217
|
+
throw new IllegalStateException("Bundle version " + hash + " not found.");
|
|
218
218
|
}
|
|
219
219
|
StateCoreResult currentState = getStateSnapshot();
|
|
220
220
|
StateCoreResult nextState = runStateCore(
|
|
@@ -385,7 +385,11 @@ public class UpdateContext {
|
|
|
385
385
|
return defaultAssetsUrl;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
|
|
388
|
+
// Guard the rollback chain against cycles: a corrupted state returning
|
|
389
|
+
// an already-visited version would otherwise spin this loop forever on
|
|
390
|
+
// the main thread.
|
|
391
|
+
java.util.HashSet<String> visitedVersions = new java.util.HashSet<>();
|
|
392
|
+
while (currentVersion != null && visitedVersions.add(currentVersion)) {
|
|
389
393
|
File bundleFile = new File(rootDir, currentVersion+"/index.bundlejs");
|
|
390
394
|
if (!bundleFile.exists()) {
|
|
391
395
|
Log.e(TAG, "Bundle version " + currentVersion + " not found.");
|
|
@@ -24,7 +24,7 @@ final class UpdateEventEmitter {
|
|
|
24
24
|
|
|
25
25
|
static void sendEvent(String eventName, WritableMap params) {
|
|
26
26
|
ReactApplicationContext reactContext = getReactContext();
|
|
27
|
-
if (reactContext == null || !reactContext
|
|
27
|
+
if (reactContext == null || !hasActiveInstance(reactContext)) {
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -32,4 +32,16 @@ final class UpdateEventEmitter {
|
|
|
32
32
|
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
33
33
|
.emit(eventName, params);
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
@SuppressWarnings("deprecation")
|
|
37
|
+
private static boolean hasActiveInstance(ReactApplicationContext reactContext) {
|
|
38
|
+
try {
|
|
39
|
+
// hasActiveCatalystInstance() is always false in bridgeless mode, which
|
|
40
|
+
// silently drops every progress event on the new architecture.
|
|
41
|
+
return reactContext.hasActiveReactInstance();
|
|
42
|
+
} catch (NoSuchMethodError e) {
|
|
43
|
+
// RN < 0.68 has no hasActiveReactInstance(); fall back for old peers.
|
|
44
|
+
return reactContext.hasActiveCatalystInstance();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
35
47
|
}
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
#include <sys/types.h>
|
|
9
9
|
#include <unistd.h>
|
|
10
10
|
|
|
11
|
-
#include <set>
|
|
12
11
|
#include <vector>
|
|
13
12
|
|
|
14
13
|
extern "C" {
|
|
@@ -17,6 +16,11 @@ extern "C" {
|
|
|
17
16
|
|
|
18
17
|
namespace pushy {
|
|
19
18
|
namespace patch {
|
|
19
|
+
|
|
20
|
+
namespace internal {
|
|
21
|
+
bool g_disable_hard_links = false;
|
|
22
|
+
} // namespace internal
|
|
23
|
+
|
|
20
24
|
namespace {
|
|
21
25
|
|
|
22
26
|
constexpr size_t kCopyBufferSize = 16 * 1024;
|
|
@@ -202,6 +206,17 @@ Status CopyFile(const std::string& from, const std::string& to, bool overwrite)
|
|
|
202
206
|
}
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
// Prefer a hard link over copying bytes: unchanged files between versions
|
|
210
|
+
// are identical, so linking is O(1) per file, writes nothing to flash, and
|
|
211
|
+
// shares disk blocks. Version directories are immutable once created (patch
|
|
212
|
+
// outputs are always written as new files), so sharing the inode with the
|
|
213
|
+
// source is safe. Fall back to a byte copy whenever linking is not possible
|
|
214
|
+
// (cross-device source such as the installed app bundle, EPERM, EMLINK, or
|
|
215
|
+
// filesystems without hard-link support).
|
|
216
|
+
if (!internal::g_disable_hard_links && link(from.c_str(), to.c_str()) == 0) {
|
|
217
|
+
return Status::Ok();
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
FILE* source = std::fopen(from.c_str(), "rb");
|
|
206
221
|
if (!source) {
|
|
207
222
|
return MakeErrnoStatus("Failed to open source file " + from);
|
|
@@ -64,5 +64,11 @@ Status CleanupOldEntries(
|
|
|
64
64
|
|
|
65
65
|
bool IsSafeRelativePath(const std::string& path);
|
|
66
66
|
|
|
67
|
+
namespace internal {
|
|
68
|
+
// Test-only escape hatch: forces file copies to take the byte-copy fallback
|
|
69
|
+
// instead of hard-linking, so tests can cover both paths on one filesystem.
|
|
70
|
+
extern bool g_disable_hard_links;
|
|
71
|
+
} // namespace internal
|
|
72
|
+
|
|
67
73
|
} // namespace patch
|
|
68
74
|
} // namespace pushy
|
|
@@ -175,6 +175,81 @@ void TestApplyPatchFromFileSourceMergesAndCopies() {
|
|
|
175
175
|
Expect(!Exists(JoinPath(target, "assets/delete.txt")), "deleted asset should not be copied");
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
void TestApplyPatchMergeHardLinksUnchangedFiles() {
|
|
179
|
+
TempDir temp;
|
|
180
|
+
const std::string source = JoinPath(temp.path, "origin");
|
|
181
|
+
const std::string target = JoinPath(temp.path, "target");
|
|
182
|
+
const std::string patch = JoinPath(temp.path, "bundle.patch");
|
|
183
|
+
|
|
184
|
+
WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
|
|
185
|
+
WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
|
|
186
|
+
WriteFile(patch, "unused patch");
|
|
187
|
+
|
|
188
|
+
FakeBundlePatcher patcher("patched bundle");
|
|
189
|
+
FileSourcePatchOptions options;
|
|
190
|
+
options.source_root = source;
|
|
191
|
+
options.target_root = target;
|
|
192
|
+
options.origin_bundle_path = JoinPath(source, "index.bundlejs");
|
|
193
|
+
options.bundle_patch_path = patch;
|
|
194
|
+
options.bundle_output_path = JoinPath(target, "index.bundlejs");
|
|
195
|
+
options.manifest.copies.push_back(CopyOperation{"assets/keep.txt", "assets/copied.txt"});
|
|
196
|
+
|
|
197
|
+
Status status = ApplyPatchFromFileSource(options, patcher);
|
|
198
|
+
Expect(status.ok, status.message);
|
|
199
|
+
|
|
200
|
+
// Unchanged files must share the source inode (hard link) instead of being
|
|
201
|
+
// byte-copied: same filesystem, so the fast path has to kick in.
|
|
202
|
+
struct stat source_stat;
|
|
203
|
+
struct stat merged_stat;
|
|
204
|
+
struct stat copied_stat;
|
|
205
|
+
Expect(stat(JoinPath(source, "assets/keep.txt").c_str(), &source_stat) == 0, "stat source");
|
|
206
|
+
Expect(stat(JoinPath(target, "assets/keep.txt").c_str(), &merged_stat) == 0, "stat merged");
|
|
207
|
+
Expect(stat(JoinPath(target, "assets/copied.txt").c_str(), &copied_stat) == 0, "stat copied");
|
|
208
|
+
Expect(merged_stat.st_ino == source_stat.st_ino, "merged file should hard-link the source");
|
|
209
|
+
Expect(copied_stat.st_ino == source_stat.st_ino, "manifest copy should hard-link the source");
|
|
210
|
+
Expect(source_stat.st_nlink >= 3, "source should have three names");
|
|
211
|
+
ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "merged content mismatch");
|
|
212
|
+
|
|
213
|
+
// The patched bundle must be a fresh file, never a link into the source dir.
|
|
214
|
+
struct stat bundle_stat;
|
|
215
|
+
struct stat origin_stat;
|
|
216
|
+
Expect(stat(JoinPath(target, "index.bundlejs").c_str(), &bundle_stat) == 0, "stat bundle");
|
|
217
|
+
Expect(stat(JoinPath(source, "index.bundlejs").c_str(), &origin_stat) == 0, "stat origin bundle");
|
|
218
|
+
Expect(bundle_stat.st_ino != origin_stat.st_ino, "patched bundle must not link the origin");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
void TestApplyPatchMergeFallsBackToByteCopy() {
|
|
222
|
+
TempDir temp;
|
|
223
|
+
const std::string source = JoinPath(temp.path, "origin");
|
|
224
|
+
const std::string target = JoinPath(temp.path, "target");
|
|
225
|
+
const std::string patch = JoinPath(temp.path, "bundle.patch");
|
|
226
|
+
|
|
227
|
+
WriteFile(JoinPath(source, "index.bundlejs"), "old bundle");
|
|
228
|
+
WriteFile(JoinPath(source, "assets/keep.txt"), "keep");
|
|
229
|
+
WriteFile(patch, "unused patch");
|
|
230
|
+
|
|
231
|
+
FakeBundlePatcher patcher("patched bundle");
|
|
232
|
+
FileSourcePatchOptions options;
|
|
233
|
+
options.source_root = source;
|
|
234
|
+
options.target_root = target;
|
|
235
|
+
options.origin_bundle_path = JoinPath(source, "index.bundlejs");
|
|
236
|
+
options.bundle_patch_path = patch;
|
|
237
|
+
options.bundle_output_path = JoinPath(target, "index.bundlejs");
|
|
238
|
+
|
|
239
|
+
pushy::patch::internal::g_disable_hard_links = true;
|
|
240
|
+
Status status = ApplyPatchFromFileSource(options, patcher);
|
|
241
|
+
pushy::patch::internal::g_disable_hard_links = false;
|
|
242
|
+
Expect(status.ok, status.message);
|
|
243
|
+
|
|
244
|
+
struct stat source_stat;
|
|
245
|
+
struct stat merged_stat;
|
|
246
|
+
Expect(stat(JoinPath(source, "assets/keep.txt").c_str(), &source_stat) == 0, "stat source");
|
|
247
|
+
Expect(stat(JoinPath(target, "assets/keep.txt").c_str(), &merged_stat) == 0, "stat merged");
|
|
248
|
+
Expect(merged_stat.st_ino != source_stat.st_ino, "fallback should produce an independent copy");
|
|
249
|
+
Expect(merged_stat.st_nlink == 1, "fallback copy should have a single name");
|
|
250
|
+
ExpectEq(ReadFile(JoinPath(target, "assets/keep.txt")), "keep", "fallback content mismatch");
|
|
251
|
+
}
|
|
252
|
+
|
|
178
253
|
void TestApplyPatchFromFileSourceCanLimitMergeSubdir() {
|
|
179
254
|
TempDir temp;
|
|
180
255
|
const std::string source = JoinPath(temp.path, "origin");
|
|
@@ -537,6 +612,8 @@ void TestStateCoreSwitchToSameVersion() {
|
|
|
537
612
|
int main() {
|
|
538
613
|
const std::vector<std::pair<std::string, void (*)()>> tests = {
|
|
539
614
|
{"ApplyPatchFromFileSourceMergesAndCopies", TestApplyPatchFromFileSourceMergesAndCopies},
|
|
615
|
+
{"ApplyPatchMergeHardLinksUnchangedFiles", TestApplyPatchMergeHardLinksUnchangedFiles},
|
|
616
|
+
{"ApplyPatchMergeFallsBackToByteCopy", TestApplyPatchMergeFallsBackToByteCopy},
|
|
540
617
|
{"ApplyPatchFromFileSourceCanLimitMergeSubdir", TestApplyPatchFromFileSourceCanLimitMergeSubdir},
|
|
541
618
|
{"ApplyPatchFromFileSourceRejectsUnsafePaths", TestApplyPatchFromFileSourceRejectsUnsafePaths},
|
|
542
619
|
{"CleanupOldEntriesRemovesOnlyExpiredPaths", TestCleanupOldEntriesRemovesOnlyExpiredPaths},
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
#include "state_ops.h"
|
|
12
12
|
|
|
13
13
|
extern "C" {
|
|
14
|
-
#include "hpatch.h"
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
namespace {
|
|
@@ -407,96 +406,6 @@ pushy::patch::PatchManifest BuildManifest(
|
|
|
407
406
|
return manifest;
|
|
408
407
|
}
|
|
409
408
|
|
|
410
|
-
napi_value HdiffPatch(napi_env env, napi_callback_info info) {
|
|
411
|
-
napi_value args[2] = {nullptr, nullptr};
|
|
412
|
-
size_t argc = 2;
|
|
413
|
-
if (!GetArgCount(env, info, &argc, args) || argc < 2) {
|
|
414
|
-
ThrowError(env, "Wrong number of arguments");
|
|
415
|
-
return nullptr;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
bool is_typed_array = false;
|
|
419
|
-
if (napi_is_typedarray(env, args[0], &is_typed_array) != napi_ok || !is_typed_array) {
|
|
420
|
-
ThrowError(env, "First argument must be a TypedArray");
|
|
421
|
-
return nullptr;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
uint8_t* origin_ptr = nullptr;
|
|
425
|
-
size_t origin_length = 0;
|
|
426
|
-
if (napi_get_typedarray_info(
|
|
427
|
-
env,
|
|
428
|
-
args[0],
|
|
429
|
-
nullptr,
|
|
430
|
-
&origin_length,
|
|
431
|
-
reinterpret_cast<void**>(&origin_ptr),
|
|
432
|
-
nullptr,
|
|
433
|
-
nullptr) != napi_ok) {
|
|
434
|
-
ThrowError(env, "Failed to get origin buffer");
|
|
435
|
-
return nullptr;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (napi_is_typedarray(env, args[1], &is_typed_array) != napi_ok || !is_typed_array) {
|
|
439
|
-
ThrowError(env, "Second argument must be a TypedArray");
|
|
440
|
-
return nullptr;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
uint8_t* patch_ptr = nullptr;
|
|
444
|
-
size_t patch_length = 0;
|
|
445
|
-
if (napi_get_typedarray_info(
|
|
446
|
-
env,
|
|
447
|
-
args[1],
|
|
448
|
-
nullptr,
|
|
449
|
-
&patch_length,
|
|
450
|
-
reinterpret_cast<void**>(&patch_ptr),
|
|
451
|
-
nullptr,
|
|
452
|
-
nullptr) != napi_ok) {
|
|
453
|
-
ThrowError(env, "Failed to get patch buffer");
|
|
454
|
-
return nullptr;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
hpatch_singleCompressedDiffInfo patch_info;
|
|
458
|
-
if (!((origin_length == 0) || origin_ptr) || !patch_ptr || patch_length == 0) {
|
|
459
|
-
ThrowError(env, "Corrupt patch");
|
|
460
|
-
return nullptr;
|
|
461
|
-
}
|
|
462
|
-
if (kHPatch_ok != hpatch_getInfo_by_mem(&patch_info, patch_ptr, patch_length)) {
|
|
463
|
-
ThrowError(env, "Error info in hpatch");
|
|
464
|
-
return nullptr;
|
|
465
|
-
}
|
|
466
|
-
if (origin_length != patch_info.oldDataSize) {
|
|
467
|
-
ThrowError(env, "Error oldDataSize in hpatch");
|
|
468
|
-
return nullptr;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
size_t new_size = static_cast<size_t>(patch_info.newDataSize);
|
|
472
|
-
if (sizeof(size_t) != sizeof(hpatch_StreamPos_t) &&
|
|
473
|
-
new_size != patch_info.newDataSize) {
|
|
474
|
-
ThrowError(env, "Error newDataSize in hpatch");
|
|
475
|
-
return nullptr;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
void* output_data = nullptr;
|
|
479
|
-
napi_value result = nullptr;
|
|
480
|
-
if (napi_create_arraybuffer(env, new_size, &output_data, &result) != napi_ok) {
|
|
481
|
-
ThrowError(env, "Failed to create result buffer");
|
|
482
|
-
return nullptr;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (kHPatch_ok != hpatch_by_mem(
|
|
486
|
-
origin_ptr,
|
|
487
|
-
origin_length,
|
|
488
|
-
static_cast<uint8_t*>(output_data),
|
|
489
|
-
new_size,
|
|
490
|
-
patch_ptr,
|
|
491
|
-
patch_length,
|
|
492
|
-
&patch_info)) {
|
|
493
|
-
ThrowError(env, "hpatch");
|
|
494
|
-
return nullptr;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
return result;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
409
|
napi_value SyncStateWithBinaryVersion(napi_env env, napi_callback_info info) {
|
|
501
410
|
napi_value args[3] = {nullptr, nullptr, nullptr};
|
|
502
411
|
size_t argc = 3;
|
|
@@ -968,8 +877,7 @@ bool ExportFunction(
|
|
|
968
877
|
} // namespace
|
|
969
878
|
|
|
970
879
|
napi_value Init(napi_env env, napi_value exports) {
|
|
971
|
-
if (!ExportFunction(env, exports, "
|
|
972
|
-
!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
|
|
880
|
+
if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
|
|
973
881
|
!ExportFunction(env, exports, "runStateCore", RunStateCore) ||
|
|
974
882
|
!ExportFunction(env, exports, "buildArchivePatchPlan", BuildArchivePatchPlan) ||
|
|
975
883
|
!ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) ||
|
|
@@ -69,7 +69,7 @@ const FILE_COPY_BUFFER_SIZE = 64 * 1024;
|
|
|
69
69
|
|
|
70
70
|
export class DownloadTask {
|
|
71
71
|
private context: common.Context;
|
|
72
|
-
private hash
|
|
72
|
+
private hash = '';
|
|
73
73
|
private eventHub: EventHub;
|
|
74
74
|
|
|
75
75
|
constructor(context: common.Context) {
|
|
@@ -244,7 +244,7 @@ export class DownloadTask {
|
|
|
244
244
|
bundleOutputPath: outputFile,
|
|
245
245
|
enableMerge: false,
|
|
246
246
|
});
|
|
247
|
-
} catch (error) {
|
|
247
|
+
} catch (error: any) {
|
|
248
248
|
error.message = `Failed to process bundle patch: ${error.message}`;
|
|
249
249
|
throw error;
|
|
250
250
|
} finally {
|
|
@@ -395,17 +395,18 @@ export class DownloadTask {
|
|
|
395
395
|
fileIo.OpenMode.CREATE | fileIo.OpenMode.READ_WRITE,
|
|
396
396
|
);
|
|
397
397
|
|
|
398
|
-
httpRequest.on('headersReceive', (header:
|
|
398
|
+
httpRequest.on('headersReceive', (header: Object) => {
|
|
399
399
|
if (!header) {
|
|
400
400
|
return;
|
|
401
401
|
}
|
|
402
|
-
const
|
|
402
|
+
const headers = header as Record<string, string>;
|
|
403
|
+
const lengthKey = Object.keys(headers).find(
|
|
403
404
|
key => key.toLowerCase() === 'content-length',
|
|
404
405
|
);
|
|
405
406
|
if (!lengthKey) {
|
|
406
407
|
return;
|
|
407
408
|
}
|
|
408
|
-
const length = parseInt(
|
|
409
|
+
const length = parseInt(headers[lengthKey], 10);
|
|
409
410
|
if (!Number.isNaN(length)) {
|
|
410
411
|
contentLength = length;
|
|
411
412
|
}
|
|
@@ -606,9 +607,11 @@ export class DownloadTask {
|
|
|
606
607
|
}
|
|
607
608
|
|
|
608
609
|
if (currentFrom.startsWith('resources/base/media/')) {
|
|
610
|
+
// Strip only the final extension: 'icon.round.png' -> 'icon.round',
|
|
611
|
+
// not 'icon' (getMediaByName expects the full resource name).
|
|
609
612
|
const mediaName = currentFrom
|
|
610
613
|
.replace('resources/base/media/', '')
|
|
611
|
-
.
|
|
614
|
+
.replace(/\.[^.]+$/, '');
|
|
612
615
|
const mediaBuffer = await resourceManager.getMediaByName(mediaName);
|
|
613
616
|
const parentDirs = [
|
|
614
617
|
...new Set(
|
|
@@ -649,7 +652,7 @@ export class DownloadTask {
|
|
|
649
652
|
}
|
|
650
653
|
}
|
|
651
654
|
}
|
|
652
|
-
} catch (error) {
|
|
655
|
+
} catch (error: any) {
|
|
653
656
|
error.message =
|
|
654
657
|
'Copy from resource failed:' +
|
|
655
658
|
currentFrom +
|
|
@@ -670,7 +673,7 @@ export class DownloadTask {
|
|
|
670
673
|
params.originHash || '',
|
|
671
674
|
3,
|
|
672
675
|
);
|
|
673
|
-
} catch (error) {
|
|
676
|
+
} catch (error: any) {
|
|
674
677
|
error.message = 'Cleanup failed:' + error.message;
|
|
675
678
|
console.error(error);
|
|
676
679
|
throw error;
|
|
@@ -698,7 +701,7 @@ export class DownloadTask {
|
|
|
698
701
|
default:
|
|
699
702
|
throw Error(`Unknown task type: ${params.type}`);
|
|
700
703
|
}
|
|
701
|
-
} catch (error) {
|
|
704
|
+
} catch (error: any) {
|
|
702
705
|
console.error('Task execution failed:', error.message);
|
|
703
706
|
if (params.type !== DownloadTaskParams.TASK_TYPE_CLEANUP) {
|
|
704
707
|
try {
|
|
@@ -707,7 +710,7 @@ export class DownloadTask {
|
|
|
707
710
|
} else {
|
|
708
711
|
await this.removeDirectory(params.unzipDirectory);
|
|
709
712
|
}
|
|
710
|
-
} catch (cleanupError) {
|
|
713
|
+
} catch (cleanupError: any) {
|
|
711
714
|
console.error('Cleanup after error failed:', cleanupError.message);
|
|
712
715
|
}
|
|
713
716
|
}
|
|
@@ -50,10 +50,6 @@ export interface FileSourcePatchRequest {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
interface NativePatchCoreBindings {
|
|
53
|
-
hdiffPatch(
|
|
54
|
-
origin: Uint8Array,
|
|
55
|
-
patch: Uint8Array,
|
|
56
|
-
): ArrayBuffer | Uint8Array;
|
|
57
53
|
syncStateWithBinaryVersion(
|
|
58
54
|
packageVersion: string,
|
|
59
55
|
buildTime: string,
|
package/harmony/pushy.har
CHANGED
|
Binary file
|
package/ios/RCTPushy/RCTPushy.mm
CHANGED
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
|
|
15
15
|
#import <React/RCTConvert.h>
|
|
16
16
|
#import <React/RCTLog.h>
|
|
17
|
+
#import <os/lock.h>
|
|
18
|
+
|
|
19
|
+
#include <atomic>
|
|
17
20
|
|
|
18
21
|
static NSString *const keyPushyInfo = @"REACTNATIVECN_PUSHY_INFO_KEY";
|
|
19
22
|
static NSString *const paramPackageVersion = @"packageVersion";
|
|
@@ -52,7 +55,20 @@ typedef NS_ENUM(NSInteger, PushyType) {
|
|
|
52
55
|
//TASK_TYPE_PLAIN_DOWNLOAD=4?
|
|
53
56
|
};
|
|
54
57
|
|
|
55
|
-
static
|
|
58
|
+
static std::atomic<bool> ignoreRollback{false};
|
|
59
|
+
|
|
60
|
+
// Serializes every read-modify-write of the persisted update state. The state
|
|
61
|
+
// machine itself is a pure function (state_core), but callers run on different
|
|
62
|
+
// threads (main thread bundleURL, module method queue, _fileQueue), so the
|
|
63
|
+
// read→transform→write sequence must be atomic to avoid e.g. markSuccess being
|
|
64
|
+
// overwritten by a concurrent bundleURL and the version being rolled back.
|
|
65
|
+
static os_unfair_lock pushyStateLock = OS_UNFAIR_LOCK_INIT;
|
|
66
|
+
|
|
67
|
+
static void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) {
|
|
68
|
+
os_unfair_lock_lock(&pushyStateLock);
|
|
69
|
+
block();
|
|
70
|
+
os_unfair_lock_unlock(&pushyStateLock);
|
|
71
|
+
}
|
|
56
72
|
|
|
57
73
|
static std::string PushyToStdString(NSString *value) {
|
|
58
74
|
if (value == nil) {
|
|
@@ -221,64 +237,71 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
221
237
|
|
|
222
238
|
+ (NSURL *)bundleURL
|
|
223
239
|
{
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
pushy::state::BinaryVersionSyncResult sync = pushy::state::SyncBinaryVersion(
|
|
231
|
-
state,
|
|
232
|
-
PushyToStdString(curPackageVersion),
|
|
233
|
-
PushyToStdString(curBuildTime)
|
|
234
|
-
);
|
|
235
|
-
if (sync.changed) {
|
|
236
|
-
[defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked];
|
|
237
|
-
state = sync.state;
|
|
238
|
-
PushyApplyStateToDefaults(defaults, state);
|
|
239
|
-
}
|
|
240
|
+
__block NSURL *resolvedURL = nil;
|
|
241
|
+
PushyWithStateLock(^{
|
|
242
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
243
|
+
|
|
244
|
+
NSString *curPackageVersion = [RCTPushy packageVersion];
|
|
245
|
+
NSString *curBuildTime = [RCTPushy buildTime];
|
|
240
246
|
|
|
241
|
-
|
|
242
|
-
pushy::state::
|
|
247
|
+
pushy::state::State state = PushyStateFromDefaults(defaults);
|
|
248
|
+
pushy::state::BinaryVersionSyncResult sync = pushy::state::SyncBinaryVersion(
|
|
243
249
|
state,
|
|
244
|
-
|
|
245
|
-
|
|
250
|
+
PushyToStdString(curPackageVersion),
|
|
251
|
+
PushyToStdString(curBuildTime)
|
|
246
252
|
);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
253
|
+
if (sync.changed) {
|
|
254
|
+
[defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked];
|
|
255
|
+
state = sync.state;
|
|
250
256
|
PushyApplyStateToDefaults(defaults, state);
|
|
251
257
|
}
|
|
252
|
-
if (decision.consumed_first_time) {
|
|
253
|
-
// bundleURL may be called many times, ignore rollbacks before process restarted again.
|
|
254
|
-
ignoreRollback = true;
|
|
255
|
-
[defaults setObject:@(YES) forKey:keyFirstLoadMarked];
|
|
256
|
-
}
|
|
257
258
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
state = pushy::state::Rollback(state);
|
|
259
|
+
if (!state.current_version.empty()) {
|
|
260
|
+
pushy::state::LaunchDecision decision = pushy::state::ResolveLaunchState(
|
|
261
|
+
state,
|
|
262
|
+
ignoreRollback.load(),
|
|
263
|
+
true
|
|
264
|
+
);
|
|
265
|
+
state = decision.state;
|
|
266
|
+
|
|
267
|
+
if (decision.did_rollback || decision.consumed_first_time) {
|
|
268
268
|
PushyApplyStateToDefaults(defaults, state);
|
|
269
|
-
|
|
269
|
+
}
|
|
270
|
+
if (decision.consumed_first_time) {
|
|
271
|
+
// bundleURL may be called many times, ignore rollbacks before process restarted again.
|
|
272
|
+
ignoreRollback = true;
|
|
273
|
+
[defaults setObject:@(YES) forKey:keyFirstLoadMarked];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
NSString *loadVersion = PushyFromStdString(decision.load_version);
|
|
277
|
+
NSString *downloadDir = [RCTPushy downloadDir];
|
|
278
|
+
while (loadVersion.length) {
|
|
279
|
+
NSString *bundlePath = [[downloadDir stringByAppendingPathComponent:loadVersion] stringByAppendingPathComponent:BUNDLE_FILE_NAME];
|
|
280
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePath isDirectory:NULL]) {
|
|
281
|
+
resolvedURL = [NSURL fileURLWithPath:bundlePath];
|
|
282
|
+
return;
|
|
283
|
+
} else {
|
|
284
|
+
RCTLogError(@"RCTPushy -- bundle version %@ not found", loadVersion);
|
|
285
|
+
state = pushy::state::Rollback(state);
|
|
286
|
+
PushyApplyStateToDefaults(defaults, state);
|
|
287
|
+
loadVersion = PushyFromStdString(state.current_version);
|
|
288
|
+
}
|
|
270
289
|
}
|
|
271
290
|
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return [RCTPushy binaryBundleURL];
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return resolvedURL ?: [RCTPushy binaryBundleURL];
|
|
275
294
|
}
|
|
276
295
|
|
|
277
296
|
+ (NSString *) rollback {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
297
|
+
__block NSString *currentVersion = nil;
|
|
298
|
+
PushyWithStateLock(^{
|
|
299
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
300
|
+
pushy::state::State state = pushy::state::Rollback(PushyStateFromDefaults(defaults));
|
|
301
|
+
PushyApplyStateToDefaults(defaults, state);
|
|
302
|
+
currentVersion = PushyFromStdString(state.current_version);
|
|
303
|
+
});
|
|
304
|
+
return currentVersion;
|
|
282
305
|
}
|
|
283
306
|
|
|
284
307
|
+ (BOOL)requiresMainQueueSetup
|
|
@@ -288,36 +311,37 @@ RCT_EXPORT_MODULE(RCTPushy);
|
|
|
288
311
|
|
|
289
312
|
- (NSDictionary *)constantsToExport
|
|
290
313
|
{
|
|
291
|
-
NSUserDefaults *defaults = PushyDefaults();
|
|
292
|
-
|
|
293
314
|
NSMutableDictionary *ret = [NSMutableDictionary new];
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
315
|
+
PushyWithStateLock(^{
|
|
316
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
317
|
+
|
|
318
|
+
ret[@"downloadRootDir"] = [RCTPushy downloadDir];
|
|
319
|
+
ret[@"packageVersion"] = [RCTPushy packageVersion];
|
|
320
|
+
ret[@"buildTime"] = [RCTPushy buildTime];
|
|
321
|
+
ret[@"rolledBackVersion"] = [defaults objectForKey:keyRolledBackMarked];
|
|
322
|
+
ret[@"isFirstTime"] = [defaults objectForKey:keyFirstLoadMarked];
|
|
323
|
+
ret[@"uuid"] = [defaults objectForKey:keyUuid];
|
|
324
|
+
NSDictionary *pushyInfo = [defaults dictionaryForKey:keyPushyInfo];
|
|
325
|
+
NSString *currentVersion = [pushyInfo objectForKey:paramCurrentVersion];
|
|
326
|
+
ret[@"currentVersion"] = currentVersion;
|
|
327
|
+
if (currentVersion != nil) {
|
|
328
|
+
ret[@"currentVersionInfo"] = [defaults objectForKey:PushyHashInfoKey(currentVersion)];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (ret[@"isFirstTime"]) {
|
|
332
|
+
[defaults removeObjectForKey:keyFirstLoadMarked];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (ret[@"rolledBackVersion"] != nil) {
|
|
336
|
+
[defaults removeObjectForKey:keyRolledBackMarked];
|
|
337
|
+
[self clearInvalidFiles];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if ([[defaults objectForKey:KeyPackageUpdatedMarked] boolValue]) {
|
|
341
|
+
[defaults removeObjectForKey:KeyPackageUpdatedMarked];
|
|
342
|
+
[self clearInvalidFiles];
|
|
343
|
+
}
|
|
344
|
+
});
|
|
321
345
|
|
|
322
346
|
return ret;
|
|
323
347
|
}
|
|
@@ -438,13 +462,15 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
438
462
|
resolve(@true);
|
|
439
463
|
#else
|
|
440
464
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
pushy::state::
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
465
|
+
PushyWithStateLock(^{
|
|
466
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
467
|
+
pushy::state::MarkSuccessResult result =
|
|
468
|
+
pushy::state::MarkSuccess(PushyStateFromDefaults(defaults));
|
|
469
|
+
if (!result.stale_version_to_delete.empty()) {
|
|
470
|
+
[defaults removeObjectForKey:PushyHashInfoKey(PushyFromStdString(result.stale_version_to_delete))];
|
|
471
|
+
}
|
|
472
|
+
PushyApplyStateToDefaults(defaults, result.state);
|
|
473
|
+
});
|
|
448
474
|
|
|
449
475
|
[self clearInvalidFiles];
|
|
450
476
|
resolve(@true);
|
|
@@ -674,38 +700,39 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
674
700
|
return NO;
|
|
675
701
|
}
|
|
676
702
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
703
|
+
PushyWithStateLock(^{
|
|
704
|
+
NSUserDefaults *defaults = PushyDefaults();
|
|
705
|
+
pushy::state::State next = pushy::state::SwitchVersion(
|
|
706
|
+
PushyStateFromDefaults(defaults),
|
|
707
|
+
PushyToStdString(hash)
|
|
708
|
+
);
|
|
709
|
+
PushyApplyStateToDefaults(defaults, next);
|
|
710
|
+
// Re-enable first-load consumption and rollback checks for the newly selected bundle.
|
|
711
|
+
ignoreRollback = false;
|
|
712
|
+
});
|
|
685
713
|
return YES;
|
|
686
714
|
}
|
|
687
715
|
|
|
688
716
|
- (BOOL)ensureDirectoryExistsAtPath:(NSString *)path
|
|
689
717
|
{
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
718
|
+
// No _fileQueue hop here: that queue also runs multi-second unzip/patch
|
|
719
|
+
// work, and a dispatch_sync onto it would block the whole module method
|
|
720
|
+
// queue for the duration. createDirectoryAtPath is idempotent and
|
|
721
|
+
// thread-safe, so checking inline is fine.
|
|
722
|
+
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
723
|
+
BOOL isDirectory = NO;
|
|
724
|
+
if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory]) {
|
|
725
|
+
return isDirectory;
|
|
726
|
+
}
|
|
699
727
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
});
|
|
728
|
+
NSError *error = nil;
|
|
729
|
+
BOOL success = [fileManager createDirectoryAtPath:path
|
|
730
|
+
withIntermediateDirectories:YES
|
|
731
|
+
attributes:nil
|
|
732
|
+
error:&error];
|
|
733
|
+
if (!success && error != nil) {
|
|
734
|
+
RCTLogWarn(@"Pushy create directory error: %@", error.localizedDescription);
|
|
735
|
+
}
|
|
709
736
|
|
|
710
737
|
return success;
|
|
711
738
|
}
|
|
@@ -741,8 +768,12 @@ RCT_EXPORT_METHOD(markSuccess:(RCTPromiseResolveBlock)resolve
|
|
|
741
768
|
- (void)clearInvalidFiles
|
|
742
769
|
{
|
|
743
770
|
dispatch_async(_fileQueue, ^{
|
|
744
|
-
|
|
745
|
-
|
|
771
|
+
// Snapshot the state under the lock, but run the (slow) filesystem
|
|
772
|
+
// cleanup outside of it so state operations are not blocked.
|
|
773
|
+
__block pushy::state::State state;
|
|
774
|
+
PushyWithStateLock(^{
|
|
775
|
+
state = PushyStateFromDefaults(PushyDefaults());
|
|
776
|
+
});
|
|
746
777
|
NSString *downloadDir = [RCTPushy downloadDir];
|
|
747
778
|
pushy::patch::Status status = pushy::patch::CleanupOldEntries(
|
|
748
779
|
PushyToStdString(downloadDir),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-update",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.45.0",
|
|
4
4
|
"description": "react-native hot update",
|
|
5
5
|
"main": "src/index",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
"publish:local": "SKIP_NATIVE_BUILD=1 npm publish",
|
|
10
10
|
"postinstall": "node scripts/check-expo-version.js",
|
|
11
11
|
"prepack": "bun submodule && bun lint",
|
|
12
|
-
"lint": "eslint \"src/*.@(ts|tsx|js|jsx)\" && tsc --noEmit",
|
|
12
|
+
"lint": "eslint \"src/*.@(ts|tsx|js|jsx)\" && tsc --noEmit && node scripts/check-harmony-types.js",
|
|
13
|
+
"lint:harmony": "node scripts/check-harmony-types.js",
|
|
13
14
|
"submodule": "git submodule update --init --recursive",
|
|
14
15
|
"test": "bun test src/__tests__",
|
|
15
16
|
"test:patch-core": "./scripts/test-patch-core.sh",
|
|
16
17
|
"build:harmony-har": "node scripts/build-harmony-har.js",
|
|
17
|
-
"build:so": "bun submodule &&
|
|
18
|
+
"build:so": "bun submodule && bash scripts/build-android-so.sh",
|
|
18
19
|
"build:ios-debug": "cd Example/e2etest && bun && detox build --configuration ios.sim.debug",
|
|
19
20
|
"build:ios-release": "cd Example/e2etest && bun && detox build --configuration ios.sim.release",
|
|
20
21
|
"test:ios-debug": "cd Example/e2etest && E2E_PLATFORM=ios detox test --configuration ios.sim.debug",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"@types/jest": "^30.0.0",
|
|
69
70
|
"@types/node": "^25.5.0",
|
|
70
71
|
"@types/react": "^19.2.14",
|
|
72
|
+
"@types/react-test-renderer": "18.0.7",
|
|
71
73
|
"detox": "^20.50.1",
|
|
72
74
|
"eslint": "^8.57.0",
|
|
73
75
|
"firebase-tools": "^15.11.0",
|
|
@@ -76,6 +78,7 @@
|
|
|
76
78
|
"prettier": "^2",
|
|
77
79
|
"react": "18.2.0",
|
|
78
80
|
"react-native": "0.73",
|
|
81
|
+
"react-test-renderer": "18.2.0",
|
|
79
82
|
"ts-jest": "^29.4.6",
|
|
80
83
|
"typescript": "^5.6.3"
|
|
81
84
|
}
|
package/src/client.ts
CHANGED
|
@@ -191,23 +191,39 @@ export class Pushy {
|
|
|
191
191
|
data?: Record<string, string | number>;
|
|
192
192
|
}) => {
|
|
193
193
|
log(`${type} ${message}`);
|
|
194
|
-
|
|
194
|
+
if (this.options.logger === noop) {
|
|
195
|
+
// Wait briefly for a logger to arrive via setOptions (e.g. the rollback
|
|
196
|
+
// report fires in the constructor before the user configures one), but
|
|
197
|
+
// give up after a bound instead of retaining the closure forever when
|
|
198
|
+
// no logger is ever provided.
|
|
199
|
+
await Promise.race([
|
|
200
|
+
this.loggerPromise.promise,
|
|
201
|
+
new Promise(resolve => setTimeout(resolve, 10 * 1000)),
|
|
202
|
+
]);
|
|
203
|
+
}
|
|
195
204
|
const { logger = noop, appKey } = this.options;
|
|
196
205
|
const overridePackageVersion = this.options.overridePackageVersion;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
206
|
+
try {
|
|
207
|
+
logger({
|
|
208
|
+
type,
|
|
209
|
+
data: {
|
|
210
|
+
appKey,
|
|
211
|
+
currentVersion,
|
|
212
|
+
cInfo,
|
|
213
|
+
packageVersion,
|
|
214
|
+
overridePackageVersion,
|
|
215
|
+
buildTime,
|
|
216
|
+
message,
|
|
217
|
+
...currentVersionInfo,
|
|
218
|
+
...data,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
} catch (e: any) {
|
|
222
|
+
// A user-provided logger must never break the update flow, and report()
|
|
223
|
+
// calls are fire-and-forget so a throw here would be an unhandled
|
|
224
|
+
// rejection.
|
|
225
|
+
log('logger error:', e?.message || e);
|
|
226
|
+
}
|
|
211
227
|
};
|
|
212
228
|
throwIfEnabled = (e: Error) => {
|
|
213
229
|
if (this.options.throwError) {
|
|
@@ -454,17 +470,6 @@ export class Pushy {
|
|
|
454
470
|
return previousRespJson ? await previousRespJson : undefined;
|
|
455
471
|
}
|
|
456
472
|
};
|
|
457
|
-
getBackupEndpoints = async () => {
|
|
458
|
-
const { server } = this.options;
|
|
459
|
-
if (!server) {
|
|
460
|
-
return [];
|
|
461
|
-
}
|
|
462
|
-
const remoteEndpoints = await this.getRemoteEndpoints();
|
|
463
|
-
return excludeConfiguredEndpoints(
|
|
464
|
-
dedupeEndpoints(remoteEndpoints),
|
|
465
|
-
this.getConfiguredCheckEndpoints(),
|
|
466
|
-
);
|
|
467
|
-
};
|
|
468
473
|
downloadUpdate = async (
|
|
469
474
|
updateInfo: CheckResult,
|
|
470
475
|
onDownloadProgress?: (data: ProgressData) => void,
|
package/src/context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useContext } from 'react';
|
|
1
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
2
2
|
import { CheckResult, ProgressData } from './type';
|
|
3
3
|
import { Pushy, Cresc } from './client';
|
|
4
4
|
import i18n from './i18n';
|
|
@@ -46,21 +46,35 @@ export const UpdateContext = createContext<{
|
|
|
46
46
|
currentHash: string;
|
|
47
47
|
packageVersion: string;
|
|
48
48
|
client?: Pushy | Cresc;
|
|
49
|
-
progress?: ProgressData;
|
|
50
49
|
updateInfo?: CheckResult;
|
|
51
50
|
lastError?: Error;
|
|
52
51
|
}>(defaultContext);
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
// Download progress ticks at high frequency, so it lives in its own context;
|
|
54
|
+
// otherwise every tick would re-render all useUpdate() consumers even when
|
|
55
|
+
// they never read progress.
|
|
56
|
+
export const ProgressContext = createContext<ProgressData | undefined>(
|
|
57
|
+
undefined,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to download progress only. Components that render a progress bar
|
|
62
|
+
* should prefer this over useUpdate() so the rest of the tree is not
|
|
63
|
+
* re-rendered on every progress event.
|
|
64
|
+
*/
|
|
65
|
+
export const useUpdateProgress = () => useContext(ProgressContext);
|
|
66
|
+
|
|
67
|
+
export const useUpdate = () => {
|
|
55
68
|
const context = useContext(UpdateContext);
|
|
69
|
+
const progress = useContext(ProgressContext);
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
if (__DEV__ && !context.client) {
|
|
72
|
+
// 检查是否在 UpdateProvider 内部使用
|
|
59
73
|
throw new Error(i18n.t('error_use_update_outside_provider'));
|
|
60
74
|
}
|
|
61
75
|
|
|
62
|
-
return context;
|
|
63
|
-
}
|
|
76
|
+
return useMemo(() => ({ ...context, progress }), [context, progress]);
|
|
77
|
+
};
|
|
64
78
|
|
|
65
79
|
/** @deprecated Please use `useUpdate` instead */
|
|
66
80
|
export const usePushy = useUpdate;
|
package/src/core.ts
CHANGED
|
@@ -82,7 +82,11 @@ export const pushyNativeEventEmitter = new NativeEventEmitter(PushyModule);
|
|
|
82
82
|
|
|
83
83
|
if (!uuid) {
|
|
84
84
|
uuid = require('nanoid/non-secure').nanoid();
|
|
85
|
-
|
|
85
|
+
// If persisting fails the uuid drifts on every launch, which skews gray
|
|
86
|
+
// release bucketing and inflates stats — log it instead of failing silently.
|
|
87
|
+
Promise.resolve(PushyModule.setUuid(uuid)).catch((e: any) => {
|
|
88
|
+
log('setUuid error:', e?.message || e);
|
|
89
|
+
});
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
export const cInfo = {
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export { Pushy, Cresc } from './client';
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
ProgressContext,
|
|
4
|
+
UpdateContext,
|
|
5
|
+
usePushy,
|
|
6
|
+
useUpdate,
|
|
7
|
+
useUpdateProgress,
|
|
8
|
+
} from './context';
|
|
3
9
|
export { PushyProvider, UpdateProvider } from './provider';
|
|
4
10
|
export { PushyModule, UpdateModule } from './core';
|
package/src/provider.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import React, {
|
|
|
2
2
|
ReactNode,
|
|
3
3
|
useCallback,
|
|
4
4
|
useEffect,
|
|
5
|
+
useMemo,
|
|
5
6
|
useRef,
|
|
6
7
|
useState,
|
|
7
8
|
} from 'react';
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
ProgressData,
|
|
25
26
|
UpdateTestPayload,
|
|
26
27
|
} from './type';
|
|
27
|
-
import { UpdateContext } from './context';
|
|
28
|
+
import { ProgressContext, UpdateContext } from './context';
|
|
28
29
|
import { URL } from 'react-native-url-polyfill';
|
|
29
30
|
import { resolveCheckResult } from './resolveCheckResult';
|
|
30
31
|
import { assertWeb, log, noop } from './utils';
|
|
@@ -44,7 +45,6 @@ export const UpdateProvider = ({
|
|
|
44
45
|
const updateInfoRef = useRef(updateInfo);
|
|
45
46
|
const [progress, setProgress] = useState<ProgressData>();
|
|
46
47
|
const [lastError, setLastError] = useState<Error>();
|
|
47
|
-
const lastChecking = useRef(0);
|
|
48
48
|
|
|
49
49
|
const throwErrorIfEnabled = useCallback(
|
|
50
50
|
(e: Error) => {
|
|
@@ -161,12 +161,9 @@ export const UpdateProvider = ({
|
|
|
161
161
|
|
|
162
162
|
const checkUpdate = useCallback(
|
|
163
163
|
async ({ extra }: { extra?: Partial<{ toHash: string }> } = {}) => {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
lastChecking.current = now;
|
|
164
|
+
// No throttle here: the client already dedupes checks via its 5s
|
|
165
|
+
// response cache, and a second throttle layer silently returned
|
|
166
|
+
// undefined, indistinguishable from a failed check.
|
|
170
167
|
let rootInfo: CheckResult | undefined;
|
|
171
168
|
try {
|
|
172
169
|
rootInfo = await client.checkUpdate(extra);
|
|
@@ -274,8 +271,9 @@ export const UpdateProvider = ({
|
|
|
274
271
|
return;
|
|
275
272
|
}
|
|
276
273
|
const { checkStrategy, autoMarkSuccess } = options;
|
|
274
|
+
let markSuccessTimer: ReturnType<typeof setTimeout> | undefined;
|
|
277
275
|
if (autoMarkSuccess) {
|
|
278
|
-
setTimeout(() => {
|
|
276
|
+
markSuccessTimer = setTimeout(() => {
|
|
279
277
|
markSuccess();
|
|
280
278
|
}, 1000);
|
|
281
279
|
}
|
|
@@ -293,6 +291,9 @@ export const UpdateProvider = ({
|
|
|
293
291
|
checkUpdate().catch(noop);
|
|
294
292
|
}
|
|
295
293
|
return () => {
|
|
294
|
+
if (markSuccessTimer) {
|
|
295
|
+
clearTimeout(markSuccessTimer);
|
|
296
|
+
}
|
|
296
297
|
stateListener.current && stateListener.current.remove();
|
|
297
298
|
};
|
|
298
299
|
}, [checkUpdate, options, dismissError, markSuccess, client]);
|
|
@@ -391,28 +392,48 @@ export const UpdateProvider = ({
|
|
|
391
392
|
};
|
|
392
393
|
}, [parseTestPayload]);
|
|
393
394
|
|
|
395
|
+
// progress lives in its own context (see context.ts), so this value only
|
|
396
|
+
// changes when the update state itself changes, not on every progress tick.
|
|
397
|
+
const contextValue = useMemo(
|
|
398
|
+
() => ({
|
|
399
|
+
checkUpdate,
|
|
400
|
+
switchVersion,
|
|
401
|
+
switchVersionLater,
|
|
402
|
+
dismissError,
|
|
403
|
+
updateInfo,
|
|
404
|
+
lastError,
|
|
405
|
+
markSuccess,
|
|
406
|
+
client,
|
|
407
|
+
downloadUpdate,
|
|
408
|
+
packageVersion,
|
|
409
|
+
currentHash: currentVersion,
|
|
410
|
+
downloadAndInstallApk,
|
|
411
|
+
getCurrentVersionInfo,
|
|
412
|
+
currentVersionInfo,
|
|
413
|
+
parseTestQrCode,
|
|
414
|
+
restartApp,
|
|
415
|
+
}),
|
|
416
|
+
[
|
|
417
|
+
checkUpdate,
|
|
418
|
+
switchVersion,
|
|
419
|
+
switchVersionLater,
|
|
420
|
+
dismissError,
|
|
421
|
+
updateInfo,
|
|
422
|
+
lastError,
|
|
423
|
+
markSuccess,
|
|
424
|
+
client,
|
|
425
|
+
downloadUpdate,
|
|
426
|
+
downloadAndInstallApk,
|
|
427
|
+
parseTestQrCode,
|
|
428
|
+
restartApp,
|
|
429
|
+
],
|
|
430
|
+
);
|
|
431
|
+
|
|
394
432
|
return (
|
|
395
|
-
<UpdateContext.Provider
|
|
396
|
-
value={
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
switchVersionLater,
|
|
400
|
-
dismissError,
|
|
401
|
-
updateInfo,
|
|
402
|
-
lastError,
|
|
403
|
-
markSuccess,
|
|
404
|
-
client,
|
|
405
|
-
downloadUpdate,
|
|
406
|
-
packageVersion,
|
|
407
|
-
currentHash: currentVersion,
|
|
408
|
-
progress,
|
|
409
|
-
downloadAndInstallApk,
|
|
410
|
-
getCurrentVersionInfo,
|
|
411
|
-
currentVersionInfo,
|
|
412
|
-
parseTestQrCode,
|
|
413
|
-
restartApp,
|
|
414
|
-
}}>
|
|
415
|
-
{children}
|
|
433
|
+
<UpdateContext.Provider value={contextValue}>
|
|
434
|
+
<ProgressContext.Provider value={progress}>
|
|
435
|
+
{children}
|
|
436
|
+
</ProgressContext.Provider>
|
|
416
437
|
</UpdateContext.Provider>
|
|
417
438
|
);
|
|
418
439
|
};
|
package/src/utils.ts
CHANGED
|
@@ -22,6 +22,10 @@ export const DEFAULT_FETCH_TIMEOUT_MS = 5000;
|
|
|
22
22
|
|
|
23
23
|
export function promiseAny<T>(promises: Promise<T>[]) {
|
|
24
24
|
return new Promise<T>((resolve, reject) => {
|
|
25
|
+
if (!promises.length) {
|
|
26
|
+
reject(Error(i18n.t('error_all_promises_rejected')));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
25
29
|
let count = 0;
|
|
26
30
|
|
|
27
31
|
promises.forEach(promise => {
|
|
@@ -118,21 +122,50 @@ export const fetchWithTimeout = (
|
|
|
118
122
|
params: Parameters<typeof fetch>[1],
|
|
119
123
|
timeoutMs = DEFAULT_FETCH_TIMEOUT_MS,
|
|
120
124
|
): Promise<Response> => {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
// AbortController landed in the RN fetch polyfill around 0.60; we support
|
|
126
|
+
// older peers, so fall back to a plain timer race when it is unavailable
|
|
127
|
+
// (the losing fetch keeps running there — old runtimes can't do better).
|
|
128
|
+
if (typeof AbortController === 'undefined') {
|
|
129
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
130
|
+
return Promise.race([
|
|
131
|
+
enhancedFetch(url, params),
|
|
132
|
+
new Promise<Response>((_, reject) => {
|
|
133
|
+
timeoutId = setTimeout(() => {
|
|
134
|
+
log('fetch timeout', url);
|
|
135
|
+
reject(Error(i18n.t('error_ping_timeout')));
|
|
136
|
+
}, timeoutMs);
|
|
137
|
+
}),
|
|
138
|
+
]).finally(() => {
|
|
139
|
+
if (timeoutId) {
|
|
140
|
+
clearTimeout(timeoutId);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Abort the underlying request on timeout instead of racing a timer: with
|
|
146
|
+
// Promise.race the losing fetch kept running (and kept the connection busy)
|
|
147
|
+
// long after the caller had already moved on.
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const timeoutId = setTimeout(() => {
|
|
150
|
+
log('fetch timeout', url);
|
|
151
|
+
controller.abort();
|
|
152
|
+
}, timeoutMs);
|
|
153
|
+
|
|
154
|
+
return enhancedFetch(url, { ...params, signal: controller.signal })
|
|
155
|
+
.catch((e: any) => {
|
|
156
|
+
if (controller.signal.aborted) {
|
|
157
|
+
throw Error(i18n.t('error_ping_timeout'));
|
|
158
|
+
}
|
|
159
|
+
throw e;
|
|
160
|
+
})
|
|
161
|
+
.finally(() => {
|
|
133
162
|
clearTimeout(timeoutId);
|
|
134
|
-
}
|
|
135
|
-
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const isIdempotentRequest = (params: Parameters<typeof fetch>[1]) => {
|
|
167
|
+
const method = params?.method?.toUpperCase() ?? 'GET';
|
|
168
|
+
return method === 'GET' || method === 'HEAD';
|
|
136
169
|
};
|
|
137
170
|
|
|
138
171
|
export const enhancedFetch = async (
|
|
@@ -143,10 +176,17 @@ export const enhancedFetch = async (
|
|
|
143
176
|
return fetch(url, params)
|
|
144
177
|
.catch(e => {
|
|
145
178
|
log('fetch error', url, e);
|
|
146
|
-
if (
|
|
179
|
+
if (
|
|
180
|
+
isRetry ||
|
|
181
|
+
(params as any)?.signal?.aborted ||
|
|
182
|
+
!url.startsWith('https:') ||
|
|
183
|
+
// Never replay non-idempotent requests (e.g. the checkUpdate POST)
|
|
184
|
+
// over plaintext http: the server may have processed the original.
|
|
185
|
+
!isIdempotentRequest(params)
|
|
186
|
+
) {
|
|
147
187
|
throw e;
|
|
148
188
|
}
|
|
149
189
|
log('trying fallback to http');
|
|
150
|
-
return enhancedFetch(url.replace(
|
|
190
|
+
return enhancedFetch(url.replace(/^https:/, 'http:'), params, true);
|
|
151
191
|
});
|
|
152
192
|
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
//
|
|
2
|
-
// Created by DengYun on 3/31/16.
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
#include "cn_reactnative_modules_update_DownloadTask.h"
|
|
6
|
-
|
|
7
|
-
#include "hpatch.h"
|
|
8
|
-
#define _check(v,errInfo) do{ if (!(v)) { _isError=hpatch_TRUE; _errInfo=errInfo; goto _clear; } }while(0)
|
|
9
|
-
|
|
10
|
-
JNIEXPORT jbyteArray JNICALL Java_cn_reactnative_modules_update_DownloadTask_hdiffPatch
|
|
11
|
-
(JNIEnv *env, jobject self, jbyteArray origin, jbyteArray patch){
|
|
12
|
-
hpatch_BOOL _isError=hpatch_FALSE;
|
|
13
|
-
const char* _errInfo="";
|
|
14
|
-
|
|
15
|
-
jbyte* originPtr = (*env)->GetByteArrayElements(env, origin, NULL);
|
|
16
|
-
size_t originLength = (*env)->GetArrayLength(env, origin);
|
|
17
|
-
jbyte* patchPtr = (*env)->GetByteArrayElements(env, patch, NULL);
|
|
18
|
-
size_t patchLength = (*env)->GetArrayLength(env, patch);
|
|
19
|
-
jbyteArray ret = NULL;
|
|
20
|
-
jbyte* outPtr = NULL;
|
|
21
|
-
size_t newsize = 0;
|
|
22
|
-
hpatch_singleCompressedDiffInfo patInfo;
|
|
23
|
-
|
|
24
|
-
_check(((originLength==0)||originPtr) && patchPtr && (patchLength>0),"Corrupt patch");
|
|
25
|
-
_check(kHPatch_ok==hpatch_getInfo_by_mem(&patInfo,(const uint8_t*)patchPtr,patchLength),"Error info in hpatch");
|
|
26
|
-
_check(originLength==patInfo.oldDataSize,"Error oldDataSize in hpatch");
|
|
27
|
-
newsize=(size_t)patInfo.newDataSize;
|
|
28
|
-
if (sizeof(size_t)!=sizeof(hpatch_StreamPos_t))
|
|
29
|
-
_check(newsize==patInfo.newDataSize,"Error newDataSize in hpatch");
|
|
30
|
-
|
|
31
|
-
ret = (*env)->NewByteArray(env,newsize);
|
|
32
|
-
_check(ret,"Error JNIEnv::NewByteArray()");
|
|
33
|
-
if (newsize>0) {
|
|
34
|
-
outPtr = (*env)->GetByteArrayElements(env, ret, NULL);
|
|
35
|
-
_check(outPtr,"Corrupt JNIEnv::GetByteArrayElements");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
_check(kHPatch_ok==hpatch_by_mem((const uint8_t*)originPtr,originLength,(uint8_t*)outPtr,newsize,
|
|
39
|
-
(const uint8_t*)patchPtr,patchLength,&patInfo),"hpacth");
|
|
40
|
-
|
|
41
|
-
_clear:
|
|
42
|
-
if (outPtr) (*env)->ReleaseByteArrayElements(env, ret, outPtr, (_isError?JNI_ABORT:0));
|
|
43
|
-
if (originPtr) (*env)->ReleaseByteArrayElements(env, origin, originPtr, JNI_ABORT);
|
|
44
|
-
if (patchPtr) (*env)->ReleaseByteArrayElements(env, patch, patchPtr, JNI_ABORT);
|
|
45
|
-
if (_isError){
|
|
46
|
-
jclass newExcCls = NULL;
|
|
47
|
-
if (ret){
|
|
48
|
-
(*env)->DeleteLocalRef(env, ret);
|
|
49
|
-
ret = NULL;
|
|
50
|
-
}
|
|
51
|
-
newExcCls = (*env)->FindClass(env, "java/lang/Error");
|
|
52
|
-
if (newExcCls != NULL) // Unable to find the new exception class, give up.
|
|
53
|
-
(*env)->ThrowNew(env, newExcCls, _errInfo);
|
|
54
|
-
}
|
|
55
|
-
return ret;
|
|
56
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/* DO NOT EDIT THIS FILE - it is machine generated */
|
|
2
|
-
#include <jni.h>
|
|
3
|
-
/* Header for class cn_reactnative_modules_update_DownloadTask */
|
|
4
|
-
|
|
5
|
-
#ifndef _Included_cn_reactnative_modules_update_DownloadTask
|
|
6
|
-
#define _Included_cn_reactnative_modules_update_DownloadTask
|
|
7
|
-
#ifdef __cplusplus
|
|
8
|
-
extern "C" {
|
|
9
|
-
#endif
|
|
10
|
-
/*
|
|
11
|
-
* Class: cn_reactnative_modules_update_DownloadTask
|
|
12
|
-
* Method: hdiffPatch
|
|
13
|
-
* Signature: ([B[B)[B
|
|
14
|
-
*/
|
|
15
|
-
JNIEXPORT jbyteArray JNICALL Java_cn_reactnative_modules_update_DownloadTask_hdiffPatch
|
|
16
|
-
(JNIEnv *, jclass, jbyteArray, jbyteArray);
|
|
17
|
-
|
|
18
|
-
#ifdef __cplusplus
|
|
19
|
-
}
|
|
20
|
-
#endif
|
|
21
|
-
#endif
|