react-native-update 10.37.19 → 10.38.0-beta.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.
Files changed (132) hide show
  1. package/README-CN.md +4 -4
  2. package/README.md +2 -12
  3. package/android/bin/.settings/org.eclipse.buildship.core.prefs +13 -0
  4. package/android/build.gradle +4 -0
  5. package/android/jni/Android.mk +14 -1
  6. package/android/jni/Application.mk +5 -2
  7. package/android/lib/arm64-v8a/librnupdate.so +0 -0
  8. package/android/lib/armeabi-v7a/librnupdate.so +0 -0
  9. package/android/lib/x86/librnupdate.so +0 -0
  10. package/android/lib/x86_64/librnupdate.so +0 -0
  11. package/android/src/main/java/cn/reactnative/modules/update/ArchivePatchPlanResult.java +6 -0
  12. package/android/src/main/java/cn/reactnative/modules/update/CopyGroupResult.java +6 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +136 -136
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateCore.java +34 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/StateCoreResult.java +16 -0
  16. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +131 -48
  17. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +88 -40
  18. package/cpp/patch_core/archive_patch_core.cpp +125 -0
  19. package/cpp/patch_core/archive_patch_core.h +59 -0
  20. package/cpp/patch_core/patch_core.cpp +533 -0
  21. package/cpp/patch_core/patch_core.h +68 -0
  22. package/cpp/patch_core/patch_core_android.cpp +112 -0
  23. package/cpp/patch_core/state_core.cpp +110 -0
  24. package/cpp/patch_core/state_core.h +58 -0
  25. package/cpp/patch_core/tests/patch_core_test.cpp +473 -0
  26. package/cpp/patch_core/update_core_android.cpp +469 -0
  27. package/harmony/pushy.har +0 -0
  28. package/ios/RCTPushy/RCTPushy.mm +233 -143
  29. package/package.json +17 -15
  30. package/react-native-update.podspec +3 -0
  31. package/scripts/build-harmony-har.js +12 -0
  32. package/scripts/prepublish.ts +49 -3
  33. package/scripts/prune-host-stl.sh +6 -0
  34. package/scripts/test-patch-core.sh +39 -0
  35. package/src/client.ts +129 -76
  36. package/src/core.ts +2 -1
  37. package/src/endpoint.ts +171 -0
  38. package/src/utils.ts +40 -27
  39. package/android/jni/lzma/DOC/7zC.txt +0 -187
  40. package/android/jni/lzma/DOC/7zFormat.txt +0 -469
  41. package/android/jni/lzma/DOC/Methods.txt +0 -173
  42. package/android/jni/lzma/DOC/installer.txt +0 -166
  43. package/android/jni/lzma/DOC/lzma-history.txt +0 -446
  44. package/android/jni/lzma/DOC/lzma-sdk.txt +0 -357
  45. package/android/jni/lzma/DOC/lzma-specification.txt +0 -1176
  46. package/android/jni/lzma/DOC/lzma.txt +0 -328
  47. package/android/jni/lzma/bin/7zS2.sfx +0 -0
  48. package/android/jni/lzma/bin/7zS2con.sfx +0 -0
  49. package/android/jni/lzma/bin/7zSD.sfx +0 -0
  50. package/android/jni/lzma/bin/7zdec.exe +0 -0
  51. package/android/jni/lzma/bin/7zr.exe +0 -0
  52. package/android/jni/lzma/bin/installer/config.txt +0 -5
  53. package/android/jni/lzma/bin/installer/cr.bat +0 -5
  54. package/android/jni/lzma/bin/lzma.exe +0 -0
  55. package/android/jni/lzma/bin/x64/7zr.exe +0 -0
  56. package/error.js +0 -1609
  57. package/harmony/har-wrapper/AppScope/app.json5 +0 -8
  58. package/harmony/har-wrapper/build-profile.json5 +0 -35
  59. package/harmony/har-wrapper/hvigor/hvigor-config.json5 +0 -5
  60. package/harmony/har-wrapper/hvigorfile.ts +0 -6
  61. package/harmony/har-wrapper/oh-package.json5 +0 -4
  62. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cache-v2-77b153ce45aba0ed28ef.json +0 -1415
  63. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/cmakeFiles-v1-b65a07793384e0ce3e08.json +0 -809
  64. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/codemodel-v2-ce0e89410afd8bf3a057.json +0 -60
  65. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/directory-.-Release-f5ebdc15457944623624.json +0 -14
  66. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/index-2026-03-18T12-02-38-0668.json +0 -89
  67. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.cmake/api/v1/reply/target-rnupdate-Release-267153624504c9c3ffdd.json +0 -222
  68. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.ninja_deps +0 -0
  69. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/.ninja_log +0 -8
  70. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeCache.txt +0 -415
  71. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCCompiler.cmake +0 -74
  72. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeCXXCompiler.cmake +0 -85
  73. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_C.bin +0 -0
  74. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeDetermineCompilerABI_CXX.bin +0 -0
  75. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CMakeSystem.cmake +0 -15
  76. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.c +0 -880
  77. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdC/CMakeCCompilerId.o +0 -0
  78. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -869
  79. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/3.28.2/CompilerIdCXX/CMakeCXXCompilerId.o +0 -0
  80. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/CMakeConfigureLog.yaml +0 -388
  81. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/TargetDirectories.txt +0 -3
  82. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/cmake.check_cache +0 -1
  83. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/HDiffPatch/file_for_patch.c.o +0 -0
  84. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/HDiffPatch/libHDiffPatch/HPatch/patch.c.o +0 -0
  85. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/hpatch.c.o +0 -0
  86. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/lzma/C/Lzma2Dec.c.o +0 -0
  87. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/__w/react-native-update/react-native-update/android/jni/lzma/C/LzmaDec.c.o +0 -0
  88. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rnupdate.dir/pushy.c.o +0 -0
  89. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/CMakeFiles/rules.ninja +0 -64
  90. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/additional_project_files.txt +0 -0
  91. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/build.ninja +0 -206
  92. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/build_file_index.txt +0 -1
  93. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/cmake_install.cmake +0 -54
  94. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/compile_commands.json +0 -38
  95. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/configure_fingerprint.json +0 -1
  96. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/hvigor_native_config.json +0 -1
  97. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/metadata_generation_command.txt +0 -17
  98. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/native_work_dir.txt +0 -1
  99. package/harmony/pushy/.cxx/default/default/release/arm64-v8a/output.log +0 -14
  100. package/harmony/pushy/.cxx/default/default/release/hvigor/arm64-v8a/summary.cmake +0 -0
  101. package/harmony/pushy/BuildProfile.ets +0 -17
  102. package/harmony/pushy/OAT.xml +0 -38
  103. package/harmony/pushy/README.md +0 -0
  104. package/harmony/pushy/build-profile.json5 +0 -15
  105. package/harmony/pushy/hvigor-plugin.ts +0 -34
  106. package/harmony/pushy/hvigorfile.ts +0 -1
  107. package/harmony/pushy/index.ets +0 -2
  108. package/harmony/pushy/oh-package-lock.json5 +0 -20
  109. package/harmony/pushy/oh-package.json5 +0 -13
  110. package/harmony/pushy/src/main/cpp/CMakeLists.txt +0 -51
  111. package/harmony/pushy/src/main/cpp/PushyPackage.h +0 -55
  112. package/harmony/pushy/src/main/cpp/PushyTurboModule.cpp +0 -142
  113. package/harmony/pushy/src/main/cpp/PushyTurboModule.h +0 -38
  114. package/harmony/pushy/src/main/cpp/pushy.c +0 -117
  115. package/harmony/pushy/src/main/cpp/pushy.h +0 -8
  116. package/harmony/pushy/src/main/ets/DownloadTask.ts +0 -570
  117. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +0 -19
  118. package/harmony/pushy/src/main/ets/EventHub.ts +0 -39
  119. package/harmony/pushy/src/main/ets/Logger.ts +0 -52
  120. package/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +0 -50
  121. package/harmony/pushy/src/main/ets/PushyPackage.ts +0 -22
  122. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +0 -171
  123. package/harmony/pushy/src/main/ets/SaveFile.ts +0 -34
  124. package/harmony/pushy/src/main/ets/UpdateContext.ts +0 -262
  125. package/harmony/pushy/src/main/ets/UpdateModuleImpl.ts +0 -123
  126. package/harmony/pushy/src/main/module.json5 +0 -7
  127. package/harmony/pushy/src/main/resources/base/element/string.json +0 -8
  128. package/harmony/pushy/src/main/resources/en_US/element/string.json +0 -8
  129. package/harmony/pushy/src/main/resources/zh_CN/element/string.json +0 -8
  130. package/harmony/pushy/ts.ts +0 -3
  131. package/src/__tests__/core.test.ts +0 -103
  132. package/src/__tests__/utils.test.ts +0 -36
@@ -0,0 +1,39 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ ROOT_DIR="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
5
+ BUILD_DIR="$ROOT_DIR/.tmp/patch-core-tests"
6
+
7
+ mkdir -p "$BUILD_DIR"
8
+
9
+ COMMON_INCLUDES="
10
+ -I$ROOT_DIR/cpp/patch_core
11
+ -I$ROOT_DIR/android/jni
12
+ -I$ROOT_DIR/android/jni/HDiffPatch
13
+ -I$ROOT_DIR/android/jni/HDiffPatch/libHDiffPatch/HPatch
14
+ -I$ROOT_DIR/android/jni/lzma/C
15
+ "
16
+
17
+ cc -Wall -Wextra $COMMON_INCLUDES -c "$ROOT_DIR/android/jni/hpatch.c" -o "$BUILD_DIR/hpatch.o"
18
+ cc -Wall -Wextra $COMMON_INCLUDES -c "$ROOT_DIR/android/jni/HDiffPatch/libHDiffPatch/HPatch/patch.c" -o "$BUILD_DIR/patch.o"
19
+ cc -Wall -Wextra $COMMON_INCLUDES -c "$ROOT_DIR/android/jni/HDiffPatch/file_for_patch.c" -o "$BUILD_DIR/file_for_patch.o"
20
+ cc -Wall -Wextra $COMMON_INCLUDES -c "$ROOT_DIR/android/jni/lzma/C/LzmaDec.c" -o "$BUILD_DIR/LzmaDec.o"
21
+ cc -Wall -Wextra $COMMON_INCLUDES -c "$ROOT_DIR/android/jni/lzma/C/Lzma2Dec.c" -o "$BUILD_DIR/Lzma2Dec.o"
22
+
23
+ c++ \
24
+ -std=c++17 \
25
+ -Wall \
26
+ -Wextra \
27
+ $COMMON_INCLUDES \
28
+ "$ROOT_DIR/cpp/patch_core/tests/patch_core_test.cpp" \
29
+ "$ROOT_DIR/cpp/patch_core/archive_patch_core.cpp" \
30
+ "$ROOT_DIR/cpp/patch_core/patch_core.cpp" \
31
+ "$ROOT_DIR/cpp/patch_core/state_core.cpp" \
32
+ "$BUILD_DIR/hpatch.o" \
33
+ "$BUILD_DIR/patch.o" \
34
+ "$BUILD_DIR/file_for_patch.o" \
35
+ "$BUILD_DIR/LzmaDec.o" \
36
+ "$BUILD_DIR/Lzma2Dec.o" \
37
+ -o "$BUILD_DIR/patch_core_test"
38
+
39
+ "$BUILD_DIR/patch_core_test"
package/src/client.ts CHANGED
@@ -17,11 +17,18 @@ import {
17
17
  setLocalHashInfo,
18
18
  } from './core';
19
19
  import { PermissionsAndroid } from './permissions';
20
- import { CheckResult, ClientOptions, EventType, ProgressData } from './type';
20
+ import {
21
+ CheckResult,
22
+ ClientOptions,
23
+ EventType,
24
+ ProgressData,
25
+ UpdateServerConfig,
26
+ } from './type';
21
27
  import {
22
28
  assertWeb,
29
+ DEFAULT_FETCH_TIMEOUT_MS,
23
30
  emptyObj,
24
- enhancedFetch,
31
+ fetchWithTimeout,
25
32
  info,
26
33
  joinUrls,
27
34
  log,
@@ -30,6 +37,7 @@ import {
30
37
  testUrls,
31
38
  } from './utils';
32
39
  import i18n from './i18n';
40
+ import { dedupeEndpoints, executeEndpointFallback } from './endpoint';
33
41
 
34
42
  const SERVER_PRESETS = {
35
43
  // cn
@@ -63,6 +71,19 @@ const defaultClientOptions: ClientOptions = {
63
71
  throwError: false,
64
72
  };
65
73
 
74
+ const cloneServerConfig = (
75
+ server?: UpdateServerConfig,
76
+ ): UpdateServerConfig | undefined => {
77
+ if (!server) {
78
+ return undefined;
79
+ }
80
+ return {
81
+ main: server.main,
82
+ backups: server.backups ? [...server.backups] : undefined,
83
+ queryUrls: server.queryUrls ? [...server.queryUrls] : undefined,
84
+ };
85
+ };
86
+
66
87
  export const sharedState: {
67
88
  progressHandlers: Record<string, EmitterSubscription>;
68
89
  downloadedHash?: string;
@@ -110,7 +131,7 @@ export class Pushy {
110
131
 
111
132
  constructor(options: ClientOptions, clientType?: 'Pushy' | 'Cresc') {
112
133
  this.clientType = clientType || 'Pushy';
113
- this.options.server = SERVER_PRESETS[this.clientType];
134
+ this.options.server = cloneServerConfig(SERVER_PRESETS[this.clientType]);
114
135
 
115
136
  i18n.setLocale(options.locale ?? this.clientType === 'Pushy' ? 'zh' : 'en');
116
137
 
@@ -134,7 +155,10 @@ export class Pushy {
134
155
  setOptions = (options: Partial<ClientOptions>) => {
135
156
  for (const [key, value] of Object.entries(options)) {
136
157
  if (value !== undefined) {
137
- (this.options as any)[key] = value;
158
+ (this.options as any)[key] =
159
+ key === 'server'
160
+ ? cloneServerConfig(value as UpdateServerConfig)
161
+ : value;
138
162
  if (key === 'logger') {
139
163
  this.loggerPromise.resolve();
140
164
  }
@@ -188,6 +212,90 @@ export class Pushy {
188
212
  getCheckUrl = (endpoint: string = this.options.server!.main) => {
189
213
  return `${endpoint}/checkUpdate/${this.options.appKey}`;
190
214
  };
215
+ getConfiguredCheckEndpoints = () => {
216
+ const { server } = this.options;
217
+ if (!server) {
218
+ return [];
219
+ }
220
+ return dedupeEndpoints([server.main, ...(server.backups || [])]);
221
+ };
222
+ getRemoteEndpoints = async () => {
223
+ const { server } = this.options;
224
+ if (!server?.queryUrls?.length) {
225
+ return [];
226
+ }
227
+ try {
228
+ const resp = await promiseAny(
229
+ server.queryUrls.map(queryUrl =>
230
+ fetchWithTimeout(queryUrl, {}, DEFAULT_FETCH_TIMEOUT_MS),
231
+ ),
232
+ );
233
+ const remoteEndpoints = await resp.json();
234
+ log('fetch endpoints:', remoteEndpoints);
235
+ if (Array.isArray(remoteEndpoints)) {
236
+ const normalizedRemoteEndpoints = dedupeEndpoints(
237
+ remoteEndpoints.filter(
238
+ (endpoint): endpoint is string => typeof endpoint === 'string',
239
+ ),
240
+ ).filter(endpoint => endpoint !== server.main);
241
+ server.backups = dedupeEndpoints([
242
+ ...(server.backups || []),
243
+ ...normalizedRemoteEndpoints,
244
+ ]).filter(endpoint => endpoint !== server.main);
245
+ return normalizedRemoteEndpoints;
246
+ }
247
+ } catch (e) {
248
+ log('failed to fetch endpoints from: ', server.queryUrls, e);
249
+ }
250
+ return [];
251
+ };
252
+ requestCheckResult = async (
253
+ endpoint: string,
254
+ fetchPayload: Parameters<typeof fetch>[1],
255
+ ) => {
256
+ const resp = await fetchWithTimeout(
257
+ this.getCheckUrl(endpoint),
258
+ fetchPayload,
259
+ DEFAULT_FETCH_TIMEOUT_MS,
260
+ );
261
+
262
+ if (!resp.ok) {
263
+ const respText = await resp.text();
264
+ throw Error(
265
+ this.t('error_http_status', {
266
+ status: resp.status,
267
+ statusText: respText,
268
+ }),
269
+ );
270
+ }
271
+
272
+ return (await resp.json()) as CheckResult;
273
+ };
274
+ fetchCheckResult = async (fetchPayload: Parameters<typeof fetch>[1]) => {
275
+ const { endpoint, value } = await executeEndpointFallback<CheckResult>({
276
+ configuredEndpoints: this.getConfiguredCheckEndpoints(),
277
+ getRemoteEndpoints: this.getRemoteEndpoints,
278
+ tryEndpoint: async currentEndpoint => {
279
+ try {
280
+ return await this.requestCheckResult(currentEndpoint, fetchPayload);
281
+ } catch (e) {
282
+ log('check endpoint failed', currentEndpoint, e);
283
+ throw e;
284
+ }
285
+ },
286
+ onFirstFailure: ({ error }) => {
287
+ this.report({
288
+ type: 'errorChecking',
289
+ message: this.t('error_cannot_connect_backup', {
290
+ message: error.message,
291
+ }),
292
+ });
293
+ },
294
+ });
295
+
296
+ log('check endpoint success', endpoint);
297
+ return value;
298
+ };
191
299
  assertDebug = (matter: string) => {
192
300
  if (__DEV__ && !this.options.debug) {
193
301
  info(this.t('dev_debug_disabled', { matter }));
@@ -271,95 +379,40 @@ export class Pushy {
271
379
  },
272
380
  body,
273
381
  };
274
- let resp;
382
+ const previousRespJson = this.lastRespJson;
275
383
  try {
276
384
  this.report({
277
385
  type: 'checking',
278
386
  message: this.options.appKey + ': ' + stringifyBody,
279
387
  });
280
- resp = await enhancedFetch(this.getCheckUrl(), fetchPayload);
281
- } catch (e: any) {
282
- this.report({
283
- type: 'errorChecking',
284
- message: this.t('error_cannot_connect_backup', { message: e.message }),
285
- });
286
- const backupEndpoints = await this.getBackupEndpoints().catch();
287
- if (backupEndpoints) {
288
- resp = await promiseAny(
289
- backupEndpoints.map(endpoint =>
290
- enhancedFetch(this.getCheckUrl(endpoint), fetchPayload),
291
- ),
292
- ).catch(() => {
293
- this.report({
294
- type: 'errorChecking',
295
- message: this.t('errorCheckingUseBackup'),
296
- });
297
- });
298
- } else {
299
- this.report({
300
- type: 'errorChecking',
301
- message: this.t('errorCheckingGetBackup'),
302
- });
303
- }
304
- }
305
- if (!resp) {
306
- this.report({
307
- type: 'errorChecking',
308
- message: this.t('error_cannot_connect_server'),
309
- });
310
- this.throwIfEnabled(Error('errorChecking'));
311
- return this.lastRespJson ? await this.lastRespJson : emptyObj;
312
- }
388
+ const respJsonPromise = this.fetchCheckResult(fetchPayload);
389
+ this.lastRespJson = respJsonPromise;
390
+ const result: CheckResult = await respJsonPromise;
313
391
 
314
- if (!resp.ok) {
315
- const respText = await resp.text();
316
- const errorMessage = this.t('error_http_status', {
317
- status: resp.status,
318
- statusText: respText,
319
- });
392
+ log('checking result:', result);
393
+
394
+ return result;
395
+ } catch (e: any) {
396
+ this.lastRespJson = previousRespJson;
397
+ const errorMessage =
398
+ e?.message || this.t('error_cannot_connect_server');
320
399
  this.report({
321
400
  type: 'errorChecking',
322
401
  message: errorMessage,
323
402
  });
324
- this.throwIfEnabled(Error('errorChecking: ' + errorMessage));
325
- return this.lastRespJson ? await this.lastRespJson : emptyObj;
403
+ this.throwIfEnabled(e);
404
+ return previousRespJson ? await previousRespJson : emptyObj;
326
405
  }
327
- const respJsonPromise = resp.json() as Promise<CheckResult>;
328
- this.lastRespJson = respJsonPromise;
329
-
330
- const result: CheckResult = await respJsonPromise;
331
-
332
- log('checking result:', result);
333
-
334
- return result;
335
406
  };
336
407
  getBackupEndpoints = async () => {
337
408
  const { server } = this.options;
338
409
  if (!server) {
339
410
  return [];
340
411
  }
341
- if (server.queryUrls) {
342
- try {
343
- const resp = await promiseAny(
344
- server.queryUrls.map(queryUrl => fetch(queryUrl)),
345
- );
346
- const remoteEndpoints = await resp.json();
347
- log('fetch endpoints:', remoteEndpoints);
348
- if (Array.isArray(remoteEndpoints)) {
349
- const backups = server.backups || [];
350
- const set = new Set(backups);
351
- for (const endpoint of remoteEndpoints) {
352
- set.add(endpoint);
353
- }
354
- if (set.size !== backups.length) {
355
- server.backups = Array.from(set);
356
- }
357
- }
358
- } catch (e: any) {
359
- log('failed to fetch endpoints from: ', server.queryUrls);
360
- }
361
- }
362
- return server.backups;
412
+ const remoteEndpoints = await this.getRemoteEndpoints();
413
+ return dedupeEndpoints([...(server.backups || []), ...remoteEndpoints]).filter(
414
+ endpoint => endpoint !== server.main,
415
+ );
363
416
  };
364
417
  downloadUpdate = async (
365
418
  updateInfo: CheckResult,
package/src/core.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
2
2
  import { emptyModule, error, log } from './utils';
3
3
  import i18n from './i18n';
4
+ /* eslint-disable @react-native/no-deep-imports */
4
5
  const {
5
6
  version: v,
6
7
  } = require('react-native/Libraries/Core/ReactNativeVersion');
@@ -47,7 +48,7 @@ if (currentVersionInfoString) {
47
48
  delete _currentVersionInfo.debugChannel;
48
49
  setLocalHashInfo(currentVersion, _currentVersionInfo);
49
50
  }
50
- } catch (err) {
51
+ } catch {
51
52
  error(
52
53
  i18n.t('error_parse_version_info', { info: currentVersionInfoString }),
53
54
  );
@@ -0,0 +1,171 @@
1
+ export interface EndpointAttemptSuccess<T> {
2
+ endpoint: string;
3
+ value: T;
4
+ duration: number;
5
+ }
6
+
7
+ export interface EndpointAttemptFailure {
8
+ endpoint: string;
9
+ error: Error;
10
+ }
11
+
12
+ export interface ExecuteEndpointFallbackOptions<T> {
13
+ configuredEndpoints: string[];
14
+ getRemoteEndpoints?: () => Promise<string[]>;
15
+ tryEndpoint: (endpoint: string) => Promise<T>;
16
+ random?: () => number;
17
+ now?: () => number;
18
+ onFirstFailure?: (failure: EndpointAttemptFailure) => void | Promise<void>;
19
+ }
20
+
21
+ const normalizeError = (error: unknown) => {
22
+ if (error instanceof Error) {
23
+ return error;
24
+ }
25
+ return new Error(String(error));
26
+ };
27
+
28
+ export const dedupeEndpoints = (
29
+ endpoints: Array<string | null | undefined>,
30
+ ): string[] => {
31
+ const result: string[] = [];
32
+ const visited = new Set<string>();
33
+
34
+ for (const endpoint of endpoints) {
35
+ if (!endpoint || visited.has(endpoint)) {
36
+ continue;
37
+ }
38
+ visited.add(endpoint);
39
+ result.push(endpoint);
40
+ }
41
+
42
+ return result;
43
+ };
44
+
45
+ export const pickRandomEndpoint = (
46
+ endpoints: string[],
47
+ random: () => number = Math.random,
48
+ ) => {
49
+ if (!endpoints.length) {
50
+ throw new Error('No endpoints configured');
51
+ }
52
+ return endpoints[Math.floor(random() * endpoints.length)];
53
+ };
54
+
55
+ export async function selectFastestSuccessfulEndpoint<T>(
56
+ endpoints: string[],
57
+ tryEndpoint: (endpoint: string) => Promise<T>,
58
+ now: () => number = Date.now,
59
+ ): Promise<{
60
+ successes: EndpointAttemptSuccess<T>[];
61
+ failures: EndpointAttemptFailure[];
62
+ }> {
63
+ const attempts = await Promise.all(
64
+ endpoints.map(async endpoint => {
65
+ const start = now();
66
+ try {
67
+ const value = await tryEndpoint(endpoint);
68
+ return {
69
+ ok: true as const,
70
+ endpoint,
71
+ value,
72
+ duration: now() - start,
73
+ };
74
+ } catch (error) {
75
+ return {
76
+ ok: false as const,
77
+ endpoint,
78
+ error: normalizeError(error),
79
+ };
80
+ }
81
+ }),
82
+ );
83
+
84
+ const successes: EndpointAttemptSuccess<T>[] = [];
85
+ const failures: EndpointAttemptFailure[] = [];
86
+
87
+ for (const attempt of attempts) {
88
+ if (attempt.ok) {
89
+ successes.push({
90
+ endpoint: attempt.endpoint,
91
+ value: attempt.value,
92
+ duration: attempt.duration,
93
+ });
94
+ continue;
95
+ }
96
+
97
+ failures.push({
98
+ endpoint: attempt.endpoint,
99
+ error: attempt.error,
100
+ });
101
+ }
102
+
103
+ successes.sort((left, right) => left.duration - right.duration);
104
+
105
+ return {
106
+ successes,
107
+ failures,
108
+ };
109
+ }
110
+
111
+ export async function executeEndpointFallback<T>({
112
+ configuredEndpoints,
113
+ getRemoteEndpoints,
114
+ tryEndpoint,
115
+ random = Math.random,
116
+ now = Date.now,
117
+ onFirstFailure,
118
+ }: ExecuteEndpointFallbackOptions<T>): Promise<EndpointAttemptSuccess<T>> {
119
+ const excludedEndpoints = new Set<string>();
120
+ let candidates = dedupeEndpoints(configuredEndpoints);
121
+
122
+ if (!candidates.length) {
123
+ throw new Error('No endpoints configured');
124
+ }
125
+
126
+ const firstEndpoint = pickRandomEndpoint(candidates, random);
127
+
128
+ try {
129
+ return {
130
+ endpoint: firstEndpoint,
131
+ value: await tryEndpoint(firstEndpoint),
132
+ duration: 0,
133
+ };
134
+ } catch (error) {
135
+ const firstFailure = {
136
+ endpoint: firstEndpoint,
137
+ error: normalizeError(error),
138
+ };
139
+ excludedEndpoints.add(firstEndpoint);
140
+ await onFirstFailure?.(firstFailure);
141
+ let lastError = firstFailure.error;
142
+
143
+ while (true) {
144
+ const remoteEndpoints = getRemoteEndpoints
145
+ ? await getRemoteEndpoints().catch(() => [])
146
+ : [];
147
+ candidates = dedupeEndpoints([...candidates, ...remoteEndpoints]).filter(
148
+ endpoint => !excludedEndpoints.has(endpoint),
149
+ );
150
+
151
+ if (!candidates.length) {
152
+ throw lastError;
153
+ }
154
+
155
+ const { successes, failures } = await selectFastestSuccessfulEndpoint(
156
+ candidates,
157
+ tryEndpoint,
158
+ now,
159
+ );
160
+
161
+ if (successes.length) {
162
+ return successes[0];
163
+ }
164
+
165
+ for (const failure of failures) {
166
+ excludedEndpoints.add(failure.endpoint);
167
+ lastError = failure.error;
168
+ }
169
+ }
170
+ }
171
+ }
package/src/utils.ts CHANGED
@@ -18,6 +18,7 @@ export function error(...args: any[]) {
18
18
  }
19
19
 
20
20
  export const isWeb = Platform.OS === 'web';
21
+ export const DEFAULT_FETCH_TIMEOUT_MS = 5000;
21
22
 
22
23
  export function promiseAny<T>(promises: Promise<T>[]) {
23
24
  return new Promise<T>((resolve, reject) => {
@@ -52,33 +53,23 @@ export const emptyModule = new EmptyModule();
52
53
  const ping = isWeb
53
54
  ? Promise.resolve
54
55
  : async (url: string) => {
55
- let pingFinished = false;
56
- return Promise.race([
57
- enhancedFetch(url, {
58
- method: 'HEAD',
59
- })
60
- .then(({ status, statusText, url: finalUrl }) => {
61
- pingFinished = true;
62
- if (status === 200) {
63
- return finalUrl;
64
- }
65
- log('ping failed', url, status, statusText);
66
- throw Error(i18n.t('error_ping_failed'));
67
- })
68
- .catch(e => {
69
- pingFinished = true;
70
- log('ping error', url, e);
71
- throw e;
72
- }),
73
- new Promise((_, reject) =>
74
- setTimeout(() => {
75
- reject(Error(i18n.t('error_ping_timeout')));
76
- if (!pingFinished) {
77
- log('ping timeout', url);
78
- }
79
- }, 5000),
80
- ),
81
- ]);
56
+ try {
57
+ const { status, statusText, url: finalUrl } = await fetchWithTimeout(
58
+ url,
59
+ {
60
+ method: 'HEAD',
61
+ },
62
+ DEFAULT_FETCH_TIMEOUT_MS,
63
+ );
64
+ if (status === 200) {
65
+ return finalUrl;
66
+ }
67
+ log('ping failed', url, status, statusText);
68
+ throw Error(i18n.t('error_ping_failed'));
69
+ } catch (e) {
70
+ log('ping error', url, e);
71
+ throw e;
72
+ }
82
73
  };
83
74
 
84
75
  export function joinUrls(paths: string[], fileName?: string) {
@@ -111,6 +102,28 @@ export const assertWeb = () => {
111
102
  return true;
112
103
  };
113
104
 
105
+ export const fetchWithTimeout = (
106
+ url: string,
107
+ params: Parameters<typeof fetch>[1],
108
+ timeoutMs = DEFAULT_FETCH_TIMEOUT_MS,
109
+ ): Promise<Response> => {
110
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
111
+
112
+ return Promise.race([
113
+ enhancedFetch(url, params),
114
+ new Promise<Response>((_, reject) => {
115
+ timeoutId = setTimeout(() => {
116
+ log('fetch timeout', url);
117
+ reject(Error(i18n.t('error_ping_timeout')));
118
+ }, timeoutMs);
119
+ }),
120
+ ]).finally(() => {
121
+ if (timeoutId) {
122
+ clearTimeout(timeoutId);
123
+ }
124
+ });
125
+ };
126
+
114
127
  // export const isAndroid70AndBelow = () => {
115
128
  // // android 7.0 and below devices do not support letsencrypt cert
116
129
  // // https://letsencrypt.org/2023/07/10/cross-sign-expiration/