react-native-update 10.49.0 → 10.51.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 (61) hide show
  1. package/BUNDLEHASH_MIGRATION.md +22 -2
  2. package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
  3. package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
  4. package/README-CN.md +14 -1
  5. package/README.md +14 -1
  6. package/android/jni/Android.mk +5 -1
  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/BundledResourceCopier.java +42 -9
  12. package/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +98 -22
  13. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
  16. package/android/src/main/java/cn/reactnative/modules/update/PatchFailedException.java +16 -0
  17. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +196 -42
  18. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +56 -3
  19. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  20. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  21. package/cpp/patch_core/digest.cpp +53 -0
  22. package/cpp/patch_core/digest.h +18 -0
  23. package/cpp/patch_core/patch_core.cpp +17 -1
  24. package/cpp/patch_core/patch_core.h +8 -0
  25. package/cpp/patch_core/tests/patch_core_test.cpp +99 -0
  26. package/cpp/update_flow_core/flow_json.cpp +522 -0
  27. package/cpp/update_flow_core/flow_json.h +103 -0
  28. package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
  29. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
  30. package/cpp/update_flow_core/update_flow_core.cpp +301 -0
  31. package/cpp/update_flow_core/update_flow_core.h +77 -0
  32. package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
  33. package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
  34. package/harmony/pushy/src/main/cpp/pushy.cpp +150 -1
  35. package/harmony/pushy/src/main/ets/DownloadTask.ts +133 -9
  36. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
  37. package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
  38. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
  39. package/harmony/pushy/src/main/ets/NativePatchCore.ts +16 -0
  40. package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
  41. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
  42. package/harmony/pushy/src/main/ets/UpdateContext.ts +152 -52
  43. package/harmony/pushy.har +0 -0
  44. package/ios/RCTPushy/RCTPushy.mm +840 -68
  45. package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
  46. package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
  47. package/package.json +3 -1
  48. package/react-native-update.podspec +2 -0
  49. package/src/NativePushy.ts +14 -0
  50. package/src/client.ts +439 -122
  51. package/src/endpoint.ts +105 -80
  52. package/src/error.ts +4 -0
  53. package/src/locales/en.ts +4 -0
  54. package/src/locales/zh.ts +4 -0
  55. package/src/provider.tsx +74 -9
  56. package/src/telemetry.ts +8 -0
  57. package/src/type.ts +31 -0
  58. package/src/updateFlowCore.ts +320 -0
  59. package/src/utils.ts +14 -12
  60. package/src/isInRollout.ts +0 -86
  61. package/src/resolveCheckResult.ts +0 -30
@@ -0,0 +1,320 @@
1
+ import type { CheckResult } from './type';
2
+
3
+ // 更新流程的纯决策层:无 IO、不依赖 react-native、无模块级状态,所有输入均由
4
+ // 参数传入(仅允许 type-only import)。这一层必须保持可在裸 JS 引擎中求值——
5
+ // 它就是未来 guardian bundle 的编译单元(NATIVE_CHECKUPDATE_DESIGN §5),
6
+ // IO(HTTP/下载/落盘)由调用方(现在是 client.ts,将来是原生编排)执行。
7
+
8
+ /* eslint-disable no-fallthrough */
9
+ /* eslint-disable no-bitwise */
10
+ export function murmurhash3_32_gc(key: string, seed = 0) {
11
+ let remainder: number,
12
+ bytes: number,
13
+ h1: number,
14
+ h1b: number,
15
+ c1: number,
16
+ c2: number,
17
+ k1: number,
18
+ i: number;
19
+
20
+ remainder = key.length & 3; // key.length % 4
21
+ bytes = key.length - remainder;
22
+ h1 = seed;
23
+ c1 = 0xcc9e2d51;
24
+ c2 = 0x1b873593;
25
+ i = 0;
26
+
27
+ while (i < bytes) {
28
+ k1 =
29
+ (key.charCodeAt(i) & 0xff) |
30
+ ((key.charCodeAt(++i) & 0xff) << 8) |
31
+ ((key.charCodeAt(++i) & 0xff) << 16) |
32
+ ((key.charCodeAt(++i) & 0xff) << 24);
33
+ ++i;
34
+
35
+ k1 =
36
+ ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
37
+ k1 = (k1 << 15) | (k1 >>> 17);
38
+ k1 =
39
+ ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
40
+
41
+ h1 ^= k1;
42
+ h1 = (h1 << 13) | (h1 >>> 19);
43
+ h1b =
44
+ ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
45
+ h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
46
+ }
47
+
48
+ k1 = 0;
49
+
50
+ switch (remainder) {
51
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: MurmurHash fallthrough
52
+ case 3:
53
+ k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
54
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: MurmurHash fallthrough
55
+ case 2:
56
+ k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
57
+ case 1:
58
+ k1 ^= key.charCodeAt(i) & 0xff;
59
+
60
+ k1 =
61
+ ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
62
+ 0xffffffff;
63
+ k1 = (k1 << 15) | (k1 >>> 17);
64
+ k1 =
65
+ ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
66
+ 0xffffffff;
67
+ h1 ^= k1;
68
+ }
69
+
70
+ h1 ^= key.length;
71
+
72
+ h1 ^= h1 >>> 16;
73
+ h1 =
74
+ ((h1 & 0xffff) * 0x85ebca6b +
75
+ ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
76
+ 0xffffffff;
77
+ h1 ^= h1 >>> 13;
78
+ h1 =
79
+ ((h1 & 0xffff) * 0xc2b2ae35 +
80
+ ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
81
+ 0xffffffff;
82
+ h1 ^= h1 >>> 16;
83
+
84
+ return h1 >>> 0;
85
+ }
86
+
87
+ export function isInRollout(rollout: number, uuid: string) {
88
+ return murmurhash3_32_gc(uuid) % 100 < rollout;
89
+ }
90
+
91
+ export function joinUrls(paths: string[], fileName?: string) {
92
+ if (fileName) {
93
+ return paths.map((path) => {
94
+ const normalizedPath = path.replace(/\/+$/, '');
95
+ // Keep explicit http(s) URLs for local/self-hosted update sources.
96
+ const baseUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(normalizedPath)
97
+ ? normalizedPath
98
+ : `https://${normalizedPath}`;
99
+ return `${baseUrl}/${fileName}`;
100
+ });
101
+ }
102
+ }
103
+
104
+ export const dedupeEndpoints = (
105
+ endpoints: Array<string | null | undefined>
106
+ ): string[] => {
107
+ const result: string[] = [];
108
+ const visited = new Set<string>();
109
+
110
+ for (const endpoint of endpoints) {
111
+ if (!endpoint || visited.has(endpoint)) {
112
+ continue;
113
+ }
114
+ visited.add(endpoint);
115
+ result.push(endpoint);
116
+ }
117
+
118
+ return result;
119
+ };
120
+
121
+ /**
122
+ * The endpoint plan: dedupe, then move the sampled pick to the front (load
123
+ * spreading), keeping the rest in configured order as the fallback sequence.
124
+ * `randomSample` ∈ [0, 1) is injected by the caller — this layer cannot draw
125
+ * randomness itself.
126
+ */
127
+ export function orderEndpointCandidates(
128
+ endpoints: Array<string | null | undefined>,
129
+ randomSample = 0
130
+ ): string[] {
131
+ const deduped = dedupeEndpoints(endpoints);
132
+ if (deduped.length < 2) {
133
+ return deduped;
134
+ }
135
+ const first = Math.max(
136
+ 0,
137
+ Math.min(
138
+ Number.isFinite(randomSample)
139
+ ? Math.floor(randomSample * deduped.length)
140
+ : 0,
141
+ deduped.length - 1
142
+ )
143
+ );
144
+ return [
145
+ deduped[first],
146
+ ...deduped.slice(0, first),
147
+ ...deduped.slice(first + 1),
148
+ ];
149
+ }
150
+
151
+ export interface CheckRequestInput {
152
+ /** Effective native package version (overridePackageVersion already applied). */
153
+ packageVersion: string;
154
+ /** Hash of the currently running JS version ('' when on the packaged bundle). */
155
+ currentVersion?: string;
156
+ buildTime: string;
157
+ cInfo: Record<string, string>;
158
+ supportedDiffVersion?: number;
159
+ /** '' or undefined while unknown — the field is then omitted. */
160
+ bundleHash?: string;
161
+ isDev?: boolean;
162
+ extra?: Record<string, any>;
163
+ }
164
+
165
+ export function buildCheckRequestBody({
166
+ packageVersion,
167
+ currentVersion,
168
+ buildTime,
169
+ cInfo,
170
+ supportedDiffVersion,
171
+ bundleHash,
172
+ isDev,
173
+ extra,
174
+ }: CheckRequestInput): Record<string, any> {
175
+ const body: Record<string, any> = {
176
+ packageVersion,
177
+ hash: currentVersion,
178
+ buildTime,
179
+ cInfo,
180
+ // 可消费的 diff 轨道版本(2 = hdiffv2 轨道),服务端据此门控下发
181
+ ...(supportedDiffVersion ? { diffV: supportedDiffVersion } : {}),
182
+ ...(bundleHash ? { bundleHash } : {}),
183
+ ...extra,
184
+ };
185
+ if (isDev) {
186
+ delete body.buildTime;
187
+ }
188
+ return body;
189
+ }
190
+
191
+ export interface UpdateIdentity {
192
+ packageVersion: string;
193
+ currentVersion?: string;
194
+ /** Stable client uuid — the gray-release bucketing key. */
195
+ uuid: string;
196
+ }
197
+
198
+ export function resolveCheckResult(
199
+ rootInfo: CheckResult,
200
+ identity: UpdateIdentity,
201
+ log: (...args: any[]) => void = () => {}
202
+ ): CheckResult {
203
+ const { expVersion, ...rootResult } = rootInfo;
204
+ const rollout = expVersion?.config?.rollout?.[identity.packageVersion];
205
+ if (rootResult.update && expVersion && typeof rollout === 'number') {
206
+ if (isInRollout(rollout, identity.uuid)) {
207
+ log(`${expVersion.name} in ${rollout}% rollout, continue`);
208
+ if (
209
+ typeof expVersion.hash === 'string' &&
210
+ expVersion.hash.length > 0 &&
211
+ expVersion.hash === identity.currentVersion
212
+ ) {
213
+ return { upToDate: true };
214
+ }
215
+ const info: CheckResult = {
216
+ update: true,
217
+ ...expVersion,
218
+ };
219
+ if (rootResult.paths) {
220
+ info.paths = rootResult.paths;
221
+ }
222
+ return info;
223
+ }
224
+ log(`${expVersion.name} not in ${rollout}% rollout, ignored`);
225
+ }
226
+ if (
227
+ rootResult.update &&
228
+ typeof rootResult.hash === 'string' &&
229
+ rootResult.hash.length > 0 &&
230
+ rootResult.hash === identity.currentVersion
231
+ ) {
232
+ return { upToDate: true };
233
+ }
234
+ return rootResult;
235
+ }
236
+
237
+ /**
238
+ * Whether the native orchestrator should activate a downloaded version for
239
+ * the next launch: either the client opted in via its silent strategies
240
+ * (afterDownload === 'setNeedUpdate'), or the server marked this version
241
+ * `config.forceBoot` — the per-version remote override that closes the
242
+ * brick-rescue gap for alert-strategy apps (a bricked device never runs JS,
243
+ * so activation cannot wait for it). Native-only: the JS side's interactive
244
+ * strategies are not consulted and not affected. The device-local
245
+ * rolledBackVersion guard in decideDownload still wins over forceBoot, and
246
+ * the activated version keeps the first_time crash-protection rollback.
247
+ */
248
+ export function shouldActivateAfterDownload(
249
+ info: CheckResult,
250
+ afterDownload?: string
251
+ ): boolean {
252
+ return afterDownload === 'setNeedUpdate' || !!info?.config?.forceBoot;
253
+ }
254
+
255
+ export type DownloadStrategyType = 'diff' | 'pdiff' | 'full';
256
+
257
+ export interface DownloadAttempt {
258
+ type: DownloadStrategyType;
259
+ /** Candidate URLs (paths × file name); the executor probes and picks one. */
260
+ urls: string[];
261
+ }
262
+
263
+ export interface DownloadPlan {
264
+ hash: string;
265
+ /** Ordered attempts: incremental first, full as the last resort. */
266
+ attempts: DownloadAttempt[];
267
+ /** Dev-only: nothing to fetch — treat as an immediate no-op success. */
268
+ devNoop: boolean;
269
+ }
270
+
271
+ export type DownloadDecision =
272
+ | {
273
+ action: 'none';
274
+ reason: 'noUpdate' | 'alreadyCurrent' | 'rolledBack' | 'noArtifact';
275
+ }
276
+ | ({ action: 'download' } & DownloadPlan);
277
+
278
+ export function decideDownload(
279
+ info: CheckResult,
280
+ identity: { currentVersion?: string; rolledBackVersion?: string },
281
+ isDev = false
282
+ ): DownloadDecision {
283
+ const { hash, diff, pdiff, full, paths = [] } = info;
284
+ if (!info.update || !hash) {
285
+ return { action: 'none', reason: 'noUpdate' };
286
+ }
287
+ if (hash === identity.currentVersion) {
288
+ return { action: 'none', reason: 'alreadyCurrent' };
289
+ }
290
+ if (identity.rolledBackVersion && hash === identity.rolledBackVersion) {
291
+ return { action: 'none', reason: 'rolledBack' };
292
+ }
293
+ const attempts: DownloadAttempt[] = [];
294
+ // Incremental artifacts only exist against release builds; dev goes
295
+ // straight to full (or the no-op below when there is none).
296
+ if (!isDev) {
297
+ const diffUrls = joinUrls(paths, diff);
298
+ if (diffUrls?.length) {
299
+ attempts.push({ type: 'diff', urls: diffUrls });
300
+ }
301
+ const pdiffUrls = joinUrls(paths, pdiff);
302
+ if (pdiffUrls?.length) {
303
+ attempts.push({ type: 'pdiff', urls: pdiffUrls });
304
+ }
305
+ }
306
+ const fullUrls = joinUrls(paths, full);
307
+ if (fullUrls?.length) {
308
+ attempts.push({ type: 'full', urls: fullUrls });
309
+ }
310
+ const devNoop = !!isDev && !fullUrls?.length;
311
+ if (!attempts.length && !devNoop) {
312
+ return { action: 'none', reason: 'noArtifact' };
313
+ }
314
+ return {
315
+ action: 'download',
316
+ hash,
317
+ attempts,
318
+ devNoop,
319
+ };
320
+ }
package/src/utils.ts CHANGED
@@ -76,18 +76,7 @@ const ping = isWeb
76
76
  }
77
77
  };
78
78
 
79
- export function joinUrls(paths: string[], fileName?: string) {
80
- if (fileName) {
81
- return paths.map((path) => {
82
- const normalizedPath = path.replace(/\/+$/, '');
83
- // Keep explicit http(s) URLs for local/self-hosted update sources.
84
- const baseUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(normalizedPath)
85
- ? normalizedPath
86
- : `https://${normalizedPath}`;
87
- return `${baseUrl}/${fileName}`;
88
- });
89
- }
90
- }
79
+ export { joinUrls } from './updateFlowCore';
91
80
 
92
81
  export const testUrls = async (urls?: string[]): Promise<string | null> => {
93
82
  if (!urls?.length) {
@@ -147,6 +136,15 @@ export const fetchWithTimeout = (
147
136
  // Promise.race the losing fetch kept running (and kept the connection busy)
148
137
  // long after the caller had already moved on.
149
138
  const controller = new AbortController();
139
+ // The timeout controller replaces params.signal on the fetch call, so a
140
+ // caller-provided signal (e.g. the hedged endpoint race cancelling losers)
141
+ // must be chained onto it manually.
142
+ const externalSignal = (params as any)?.signal as AbortSignal | undefined;
143
+ if (externalSignal?.aborted) {
144
+ controller.abort();
145
+ } else if (externalSignal) {
146
+ externalSignal.addEventListener('abort', () => controller.abort());
147
+ }
150
148
  const timeoutId = setTimeout(() => {
151
149
  log('fetch timeout', url);
152
150
  controller.abort();
@@ -154,6 +152,10 @@ export const fetchWithTimeout = (
154
152
 
155
153
  return enhancedFetch(url, { ...params, signal: controller.signal as any })
156
154
  .catch((e: any) => {
155
+ if (externalSignal?.aborted) {
156
+ // Cancelled by the caller, not timed out; keep the abort semantics.
157
+ throw e;
158
+ }
157
159
  if (controller.signal.aborted) {
158
160
  throw Error(i18n.t('error_ping_timeout'));
159
161
  }
@@ -1,86 +0,0 @@
1
- /* eslint-disable no-fallthrough */
2
-
3
- import { cInfo } from './core';
4
-
5
- /* eslint-disable no-bitwise */
6
- export function murmurhash3_32_gc(key: string, seed = 0) {
7
- let remainder: number,
8
- bytes: number,
9
- h1: number,
10
- h1b: number,
11
- c1: number,
12
- c2: number,
13
- k1: number,
14
- i: number;
15
-
16
- remainder = key.length & 3; // key.length % 4
17
- bytes = key.length - remainder;
18
- h1 = seed;
19
- c1 = 0xcc9e2d51;
20
- c2 = 0x1b873593;
21
- i = 0;
22
-
23
- while (i < bytes) {
24
- k1 =
25
- (key.charCodeAt(i) & 0xff) |
26
- ((key.charCodeAt(++i) & 0xff) << 8) |
27
- ((key.charCodeAt(++i) & 0xff) << 16) |
28
- ((key.charCodeAt(++i) & 0xff) << 24);
29
- ++i;
30
-
31
- ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
32
- k1 = (k1 << 15) | (k1 >>> 17);
33
- k1 =
34
- ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
35
-
36
- h1 ^= k1;
37
- h1 = (h1 << 13) | (h1 >>> 19);
38
- h1b =
39
- ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
40
- h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
41
- }
42
-
43
- k1 = 0;
44
-
45
- switch (remainder) {
46
- // biome-ignore lint/suspicious/noFallthroughSwitchClause: MurmurHash fallthrough
47
- case 3:
48
- k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
49
- // biome-ignore lint/suspicious/noFallthroughSwitchClause: MurmurHash fallthrough
50
- case 2:
51
- k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
52
- case 1:
53
- k1 ^= key.charCodeAt(i) & 0xff;
54
-
55
- k1 =
56
- ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) &
57
- 0xffffffff;
58
- k1 = (k1 << 15) | (k1 >>> 17);
59
- k1 =
60
- ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) &
61
- 0xffffffff;
62
- h1 ^= k1;
63
- }
64
-
65
- h1 ^= key.length;
66
-
67
- h1 ^= h1 >>> 16;
68
- h1 =
69
- ((h1 & 0xffff) * 0x85ebca6b +
70
- ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) &
71
- 0xffffffff;
72
- h1 ^= h1 >>> 13;
73
- h1 =
74
- ((h1 & 0xffff) * 0xc2b2ae35 +
75
- ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) &
76
- 0xffffffff;
77
- h1 ^= h1 >>> 16;
78
-
79
- return h1 >>> 0;
80
- }
81
-
82
- const intForUUID = murmurhash3_32_gc(cInfo.uuid);
83
-
84
- export function isInRollout(rollout: number) {
85
- return intForUUID % 100 < rollout;
86
- }
@@ -1,30 +0,0 @@
1
- import { currentVersion, packageVersion } from './core';
2
- import { isInRollout } from './isInRollout';
3
- import type { CheckResult } from './type';
4
- import { log } from './utils';
5
-
6
- export function resolveCheckResult(rootInfo: CheckResult): CheckResult {
7
- const { expVersion, ...rootResult } = rootInfo;
8
- const rollout = expVersion?.config?.rollout?.[packageVersion];
9
- if (rootResult.update && expVersion && typeof rollout === 'number') {
10
- if (isInRollout(rollout)) {
11
- log(`${expVersion.name} in ${rollout}% rollout, continue`);
12
- if (expVersion.hash === currentVersion) {
13
- return { upToDate: true };
14
- }
15
- const info: CheckResult = {
16
- update: true,
17
- ...expVersion,
18
- };
19
- if (rootResult.paths) {
20
- info.paths = rootResult.paths;
21
- }
22
- return info;
23
- }
24
- log(`${expVersion.name} not in ${rollout}% rollout, ignored`);
25
- }
26
- if (rootResult.update && rootResult.hash === currentVersion) {
27
- return { upToDate: true };
28
- }
29
- return rootResult;
30
- }