react-native-update 10.50.0 → 10.51.1

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 (50) hide show
  1. package/NATIVE_CHECKUPDATE_DESIGN.md +251 -21
  2. package/NATIVE_CHECK_FOLLOWUPS.md +365 -0
  3. package/README-CN.md +14 -1
  4. package/README.md +14 -1
  5. package/android/build.gradle +10 -0
  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/DownloadTask.java +76 -21
  12. package/android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java +3 -0
  13. package/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +455 -0
  14. package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.java +23 -0
  15. package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +160 -42
  16. package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +44 -0
  17. package/android/src/newarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  18. package/android/src/oldarch/cn/reactnative/modules/update/UpdateModule.java +10 -0
  19. package/cpp/update_flow_core/flow_json.cpp +522 -0
  20. package/cpp/update_flow_core/flow_json.h +103 -0
  21. package/cpp/update_flow_core/tests/flow_vectors.json +1401 -0
  22. package/cpp/update_flow_core/tests/update_flow_core_test.cpp +317 -0
  23. package/cpp/update_flow_core/update_flow_core.cpp +301 -0
  24. package/cpp/update_flow_core/update_flow_core.h +77 -0
  25. package/cpp/update_flow_core/update_flow_jni.cpp +121 -0
  26. package/harmony/pushy/src/main/cpp/CMakeLists.txt +7 -0
  27. package/harmony/pushy/src/main/cpp/pushy.cpp +103 -1
  28. package/harmony/pushy/src/main/ets/DownloadTask.ts +48 -9
  29. package/harmony/pushy/src/main/ets/DownloadTaskParams.ts +3 -0
  30. package/harmony/pushy/src/main/ets/MonotonicClock.ts +13 -0
  31. package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +500 -0
  32. package/harmony/pushy/src/main/ets/NativePatchCore.ts +14 -0
  33. package/harmony/pushy/src/main/ets/PathUtils.ts +21 -0
  34. package/harmony/pushy/src/main/ets/PushyTurboModule.ts +22 -0
  35. package/harmony/pushy/src/main/ets/UpdateContext.ts +148 -69
  36. package/harmony/pushy.har +0 -0
  37. package/ios/RCTPushy/RCTPushy.mm +779 -61
  38. package/ios/RCTPushy/RCTPushyDownloader.h +1 -0
  39. package/ios/RCTPushy/RCTPushyDownloader.mm +4 -3
  40. package/package.json +3 -1
  41. package/react-native-update.podspec +2 -0
  42. package/src/NativePushy.ts +14 -0
  43. package/src/client.ts +341 -118
  44. package/src/endpoint.ts +7 -29
  45. package/src/provider.tsx +38 -2
  46. package/src/type.ts +26 -0
  47. package/src/updateFlowCore.ts +320 -0
  48. package/src/utils.ts +1 -12
  49. package/src/isInRollout.ts +0 -87
  50. package/src/resolveCheckResult.ts +0 -30
@@ -0,0 +1,500 @@
1
+ import http from '@ohos.net.http';
2
+ import deviceInfo from '@ohos.deviceInfo';
3
+ import logger from './Logger';
4
+ import NativePatchCore from './NativePatchCore';
5
+ import type { UpdateContext } from './UpdateContext';
6
+ import { isSafePathComponent } from './PathUtils';
7
+ import { monotonicNowMs } from './MonotonicClock';
8
+
9
+ // 原生冷启动检测(NATIVE_CHECKUPDATE_DESIGN §10):每进程一次,getBundleUrl
10
+ // 后延迟数秒运行,完全不依赖 app bundle——坏热更把 JS 砸挂后,下次启动仍能
11
+ // 拉到修复版。决策全部来自 cpp/update_flow_core(经 NativePatchCore 的
12
+ // NAPI 面),本文件只是 IO 胶水。失败静默且有界:每次启动至多一轮,无重试
13
+ // 风暴,不拉黑版本。
14
+ // 鸿蒙的 debug 门控由触发点天然承担:dev 走 MetroJSBundleProvider,
15
+ // PushyFileJSBundleProvider.getBundleUrl 不会被调用。
16
+ const TAG = 'NativeCheck';
17
+ export const KEY_CONFIG = 'nativeConfig';
18
+ // 供 JS 侧复用的原始响应缓存(§10.3),同时记录请求与配置指纹以限定命中范围。
19
+ export const KEY_RESP_CACHE = 'nativeCheckResp';
20
+ const REQUEST_TIMEOUT_MS = 10000;
21
+ const REQUEST_CALL_TIMEOUT_MS = 15000;
22
+ const MAX_CHECK_HTTP_ATTEMPTS = 8;
23
+ const START_DELAY_MS = 5000;
24
+ const DOWNLOAD_PHASE_TIMEOUT_MS = 10 * 60 * 1000;
25
+ const DOWNLOAD_TYPE_DIFF = 'diff';
26
+ const DOWNLOAD_TYPE_PDIFF = 'pdiff';
27
+
28
+ interface NativeConfig {
29
+ appKey?: string;
30
+ packageVersion?: string;
31
+ endpoints?: string[];
32
+ queryUrls?: string[];
33
+ afterDownload?: string;
34
+ rnu?: string;
35
+ rn?: string;
36
+ disabled?: boolean;
37
+ }
38
+
39
+ interface FlowIdentity {
40
+ packageVersion: string;
41
+ currentVersion?: string;
42
+ uuid: string;
43
+ rolledBackVersion?: string;
44
+ }
45
+
46
+ interface FlowCInfo {
47
+ rnu: string;
48
+ rn: string;
49
+ os: string;
50
+ uuid: string;
51
+ }
52
+
53
+ interface FlowCheckInput {
54
+ packageVersion: string;
55
+ currentVersion?: string;
56
+ buildTime: string;
57
+ cInfo: FlowCInfo;
58
+ supportedDiffVersion: number;
59
+ bundleHash: string;
60
+ }
61
+
62
+ interface DecisionAttempt {
63
+ type?: string;
64
+ urls?: string[];
65
+ }
66
+
67
+ interface DecisionInfo {
68
+ name?: string;
69
+ description?: string;
70
+ metaInfo?: string;
71
+ }
72
+
73
+ interface Decision {
74
+ action?: string;
75
+ reason?: string;
76
+ hash?: string;
77
+ attempts?: DecisionAttempt[];
78
+ activate?: boolean;
79
+ info?: DecisionInfo;
80
+ }
81
+
82
+ interface RespCacheEntry {
83
+ ts: number;
84
+ body: string;
85
+ request: string;
86
+ config: string;
87
+ }
88
+
89
+ let scheduled = false;
90
+
91
+ export function scheduleNativeCheck(
92
+ context: UpdateContext,
93
+ launchRolledBackVersion: string,
94
+ ): void {
95
+ if (scheduled) {
96
+ return;
97
+ }
98
+ scheduled = true;
99
+ // 结果本来就是"下次启动生效",延迟几秒让开冷启动关键路径(§7 R5)。
100
+ setTimeout(() => {
101
+ runOnce(context, launchRolledBackVersion).catch((e: Object) => {
102
+ // 救援路径自身绝不能把应用拖垮。
103
+ logger.error(TAG, `native check failed: ${e}`);
104
+ });
105
+ }, START_DELAY_MS);
106
+ }
107
+
108
+ async function runOnce(
109
+ context: UpdateContext,
110
+ launchRolledBackVersion: string,
111
+ ): Promise<void> {
112
+ // 在任何 IO 之前采样:resetToPackagedBundle 会递增它,本轮运行期间发生的
113
+ // reset 必须赢过本轮的决策。
114
+ const resetGeneration = context.getResetGeneration();
115
+ const configJson = context.getKv(KEY_CONFIG);
116
+ if (!configJson) {
117
+ // 无落盘配置(老接入/首启):静默不跑——这就是灰度开关。
118
+ return;
119
+ }
120
+ let config: NativeConfig;
121
+ try {
122
+ config = JSON.parse(configJson) as NativeConfig;
123
+ } catch (e) {
124
+ return;
125
+ }
126
+ if (config.disabled) {
127
+ return;
128
+ }
129
+ const appKey = config.appKey ?? '';
130
+ if (!appKey) {
131
+ return;
132
+ }
133
+
134
+ const currentVersion = context.getCurrentVersion();
135
+ // getConstants consumes the persisted rollback marker during startup; use
136
+ // the launch-path snapshot captured before that happens.
137
+ const rolledBackVersion = launchRolledBackVersion;
138
+ const uuid = context.getKv('uuid') ?? '';
139
+ const packageVersion = config.packageVersion || context.getPackageVersion();
140
+
141
+ const identity: FlowIdentity = {
142
+ packageVersion,
143
+ currentVersion,
144
+ uuid,
145
+ };
146
+ if (rolledBackVersion) {
147
+ identity.rolledBackVersion = rolledBackVersion;
148
+ }
149
+
150
+ const cInfo: FlowCInfo = {
151
+ rnu: config.rnu ?? '',
152
+ rn: config.rn ?? '',
153
+ // RNOH's Platform.Version is osFullName; use the same value so JS can
154
+ // reuse the response cache produced by this native request.
155
+ os: `harmony ${deviceInfo.osFullName}`,
156
+ uuid,
157
+ };
158
+
159
+ const input: FlowCheckInput = {
160
+ packageVersion: identity.packageVersion,
161
+ currentVersion,
162
+ buildTime: context.getBuildTime(),
163
+ cInfo,
164
+ supportedDiffVersion: NativePatchCore.getSupportedDiffVersion(),
165
+ bundleHash: await context.getBundleHash(),
166
+ };
167
+ const body = NativePatchCore.buildCheckRequestBody(JSON.stringify(input));
168
+ if (!body) {
169
+ return;
170
+ }
171
+
172
+ const responseText = await runCheckRequest(config, appKey, body);
173
+ if (!responseText) {
174
+ logger.debug(TAG, 'no endpoint reachable, giving up until next launch');
175
+ return;
176
+ }
177
+ // Anchor cache freshness to response arrival, before download/patch work.
178
+ const responseAtSeconds = Math.floor(Date.now() / 1000);
179
+
180
+ const decisionJson = NativePatchCore.handleCheckResponse(
181
+ responseText,
182
+ JSON.stringify(identity),
183
+ config.afterDownload ?? '',
184
+ );
185
+ if (!decisionJson) {
186
+ return;
187
+ }
188
+ const decision = JSON.parse(decisionJson) as Decision;
189
+ if (decision.action !== 'download') {
190
+ context.commitNativeCheckResult(
191
+ resetGeneration,
192
+ '',
193
+ '',
194
+ false,
195
+ buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
196
+ );
197
+ logger.debug(TAG, `nothing to do (${decision.reason ?? ''})`);
198
+ return;
199
+ }
200
+ const hash = decision.hash ?? '';
201
+ if (!isSafePathComponent(hash)) {
202
+ return;
203
+ }
204
+
205
+ let downloaded = context.hasDownloadedVersion(hash);
206
+ if (!downloaded) {
207
+ downloaded = await performAttempts(
208
+ context,
209
+ decision.attempts ?? [],
210
+ hash,
211
+ currentVersion,
212
+ );
213
+ }
214
+ if (!downloaded) {
215
+ context.commitNativeCheckResult(
216
+ resetGeneration,
217
+ '',
218
+ '',
219
+ false,
220
+ buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
221
+ );
222
+ return;
223
+ }
224
+
225
+ // 与 JS 侧下载成功后的 setLocalHashInfo 对齐,持久化版本元信息。
226
+ const info = decision.info;
227
+ let hashInfoJson = '';
228
+ if (info) {
229
+ const hashInfo: DecisionInfo = {};
230
+ if (typeof info.name === 'string') {
231
+ hashInfo.name = info.name;
232
+ }
233
+ if (typeof info.description === 'string') {
234
+ hashInfo.description = info.description;
235
+ }
236
+ if (typeof info.metaInfo === 'string') {
237
+ hashInfo.metaInfo = info.metaInfo;
238
+ }
239
+ hashInfoJson = JSON.stringify(hashInfo);
240
+ }
241
+
242
+ // 版本元信息、激活与响应缓存一次性原子提交(见 commitNativeCheckResult);
243
+ // 缓存只在原生文件/状态工作结束后公开,避免 JS 观察到响应后并发下载。
244
+ // 静默策略、或服务端按版本标记的 forceBoot(远程覆盖,救砖指令)才激活。
245
+ const activate = decision.activate === true;
246
+ let committed = false;
247
+ try {
248
+ committed = context.commitNativeCheckResult(
249
+ resetGeneration,
250
+ hash,
251
+ hashInfoJson,
252
+ activate,
253
+ buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
254
+ );
255
+ } catch (e) {
256
+ logger.error(TAG, `commit failed: ${e}`);
257
+ return;
258
+ }
259
+ if (!committed) {
260
+ logger.debug(TAG, 'reset during round, dropping result');
261
+ } else if (activate) {
262
+ logger.debug(TAG, `downloaded ${hash} and set for next launch`);
263
+ } else {
264
+ logger.debug(TAG, `downloaded ${hash}, activation left to JS`);
265
+ }
266
+ }
267
+
268
+ function buildResponseCacheJson(
269
+ configJson: string,
270
+ requestBody: string,
271
+ responseText: string,
272
+ responseAtSeconds: number,
273
+ ): string {
274
+ const cacheEntry: RespCacheEntry = {
275
+ ts: responseAtSeconds,
276
+ body: responseText,
277
+ request: requestBody,
278
+ config: configJson,
279
+ };
280
+ return JSON.stringify(cacheEntry);
281
+ }
282
+
283
+ function isValidCheckResponse(responseText: string | undefined): boolean {
284
+ if (responseText === undefined) {
285
+ return false;
286
+ }
287
+ try {
288
+ const parsed = JSON.parse(responseText) as Object | null;
289
+ return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed);
290
+ } catch (e) {
291
+ return false;
292
+ }
293
+ }
294
+
295
+ async function httpRequest(
296
+ url: string,
297
+ postBody?: string,
298
+ ): Promise<string | undefined> {
299
+ const client = http.createHttp();
300
+ let callTimer: number | null = null;
301
+ try {
302
+ const header: Record<string, string> = { Accept: 'application/json' };
303
+ if (postBody !== undefined) {
304
+ header['Content-Type'] = 'application/json';
305
+ }
306
+ const requestPromise = client.request(url, {
307
+ method: postBody !== undefined
308
+ ? http.RequestMethod.POST
309
+ : http.RequestMethod.GET,
310
+ header,
311
+ extraData: postBody,
312
+ connectTimeout: REQUEST_TIMEOUT_MS,
313
+ readTimeout: REQUEST_TIMEOUT_MS,
314
+ expectDataType: http.HttpDataType.STRING,
315
+ });
316
+ const timeoutPromise = new Promise<http.HttpResponse>((_, reject) => {
317
+ callTimer = setTimeout(() => {
318
+ reject(Error(`HTTP request exceeded ${REQUEST_CALL_TIMEOUT_MS}ms`));
319
+ }, REQUEST_CALL_TIMEOUT_MS);
320
+ });
321
+ const response = await Promise.race([requestPromise, timeoutPromise]);
322
+ if (
323
+ response.responseCode >= 200 &&
324
+ response.responseCode < 300 &&
325
+ typeof response.result === 'string'
326
+ ) {
327
+ return response.result;
328
+ }
329
+ return undefined;
330
+ } catch (e) {
331
+ return undefined;
332
+ } finally {
333
+ if (callTimer !== null) {
334
+ clearTimeout(callTimer);
335
+ }
336
+ client.destroy();
337
+ }
338
+ }
339
+
340
+ // 顺序回退(§5.1):按纯层给出的候选序逐个请求,单请求超时;整轮失败后经
341
+ // queryUrls 发现远程候选(排除已试过的)再来一轮。刻意不做 hedged race——
342
+ // 该路径对延迟不敏感。
343
+ async function runCheckRequest(
344
+ config: NativeConfig,
345
+ appKey: string,
346
+ body: string,
347
+ ): Promise<string | undefined> {
348
+ const orderedJson = NativePatchCore.orderEndpointCandidates(
349
+ JSON.stringify(config.endpoints ?? []),
350
+ Math.random(),
351
+ );
352
+ if (!orderedJson) {
353
+ return undefined;
354
+ }
355
+ const ordered = JSON.parse(orderedJson) as string[];
356
+ const tried = new Set<string>();
357
+ let httpAttempts = 0;
358
+ for (const rawBase of ordered) {
359
+ const base = normalizeEndpointBase(rawBase);
360
+ if (!base || tried.has(base)) {
361
+ continue;
362
+ }
363
+ if (httpAttempts++ >= MAX_CHECK_HTTP_ATTEMPTS) {
364
+ return undefined;
365
+ }
366
+ tried.add(base);
367
+ const response = await httpRequest(`${base}/checkUpdate/${appKey}`, body);
368
+ if (isValidCheckResponse(response)) {
369
+ return response;
370
+ }
371
+ }
372
+ for (const listUrl of config.queryUrls ?? []) {
373
+ if (!listUrl) {
374
+ continue;
375
+ }
376
+ if (httpAttempts++ >= MAX_CHECK_HTTP_ATTEMPTS) {
377
+ return undefined;
378
+ }
379
+ const listText = await httpRequest(listUrl);
380
+ if (listText === undefined) {
381
+ continue;
382
+ }
383
+ let remote: string[];
384
+ try {
385
+ const parsed = JSON.parse(listText) as Object;
386
+ if (!Array.isArray(parsed)) {
387
+ continue;
388
+ }
389
+ remote = parsed as string[];
390
+ } catch (e) {
391
+ continue;
392
+ }
393
+ for (const rawBase of remote) {
394
+ if (typeof rawBase !== 'string') {
395
+ continue;
396
+ }
397
+ const base = normalizeEndpointBase(rawBase);
398
+ if (!base || tried.has(base)) {
399
+ continue;
400
+ }
401
+ if (httpAttempts++ >= MAX_CHECK_HTTP_ATTEMPTS) {
402
+ return undefined;
403
+ }
404
+ tried.add(base);
405
+ const response = await httpRequest(`${base}/checkUpdate/${appKey}`, body);
406
+ if (isValidCheckResponse(response)) {
407
+ return response;
408
+ }
409
+ }
410
+ // 拉到一份可解析的远程列表就够了。
411
+ break;
412
+ }
413
+ return undefined;
414
+ }
415
+
416
+ function normalizeEndpointBase(base: string): string {
417
+ return base.replace(/\/+$/, '');
418
+ }
419
+
420
+ async function runWithinDeadline(
421
+ start: () => Promise<void>,
422
+ deadlineUptimeMs: number,
423
+ ): Promise<void> {
424
+ const remainingMs = deadlineUptimeMs - monotonicNowMs();
425
+ if (remainingMs <= 0) {
426
+ throw Error('Download phase deadline expired before start');
427
+ }
428
+ let deadlineTimer = 0;
429
+ const deadlinePromise = new Promise<void>((_, reject) => {
430
+ deadlineTimer = setTimeout(() => {
431
+ reject(Error('Download phase deadline exceeded'));
432
+ }, remainingMs);
433
+ });
434
+ try {
435
+ // This bounds queueing, HTTP, decompression and native hpatch work from
436
+ // the orchestrator's perspective. The serialized task may still finish
437
+ // later, but it can no longer prevent the response cache from settling.
438
+ await Promise.race([start(), deadlinePromise]);
439
+ } finally {
440
+ clearTimeout(deadlineTimer);
441
+ }
442
+ }
443
+
444
+ async function performAttempts(
445
+ context: UpdateContext,
446
+ attempts: DecisionAttempt[],
447
+ hash: string,
448
+ originHash: string,
449
+ ): Promise<boolean> {
450
+ const incrementalDeadline = monotonicNowMs() + DOWNLOAD_PHASE_TIMEOUT_MS;
451
+ let fullDeadline = 0;
452
+ for (const attempt of attempts) {
453
+ const type = attempt.type ?? '';
454
+ if (type === DOWNLOAD_TYPE_DIFF && !originHash) {
455
+ // diff 以当前运行版本为源;没有运行中的热更版本就跳过。
456
+ continue;
457
+ }
458
+ const isFullAttempt =
459
+ type !== DOWNLOAD_TYPE_DIFF && type !== DOWNLOAD_TYPE_PDIFF;
460
+ if (isFullAttempt && fullDeadline === 0) {
461
+ // Preserve a full 10min rescue budget even when diff/pdiff exhausted
462
+ // their own phase window.
463
+ fullDeadline = monotonicNowMs() + DOWNLOAD_PHASE_TIMEOUT_MS;
464
+ }
465
+ const deadline = isFullAttempt ? fullDeadline : incrementalDeadline;
466
+ for (const url of attempt.urls ?? []) {
467
+ if (!url) {
468
+ continue;
469
+ }
470
+ if (monotonicNowMs() >= deadline) {
471
+ if (isFullAttempt) {
472
+ return false;
473
+ }
474
+ break;
475
+ }
476
+ try {
477
+ if (type === DOWNLOAD_TYPE_DIFF) {
478
+ await runWithinDeadline(
479
+ () => context.downloadPatchFromPpk(url, hash, originHash, deadline),
480
+ deadline,
481
+ );
482
+ } else if (type === DOWNLOAD_TYPE_PDIFF) {
483
+ await runWithinDeadline(
484
+ () => context.downloadPatchFromPackage(url, hash, deadline),
485
+ deadline,
486
+ );
487
+ } else {
488
+ await runWithinDeadline(
489
+ () => context.downloadFullUpdate(url, hash, deadline),
490
+ deadline,
491
+ );
492
+ }
493
+ return true;
494
+ } catch (e) {
495
+ logger.debug(TAG, `${type} attempt failed: ${e}`);
496
+ }
497
+ }
498
+ }
499
+ return false;
500
+ }
@@ -86,6 +86,20 @@ interface NativePatchCoreBindings {
86
86
  crc32(data: Uint8Array | ArrayBuffer): number;
87
87
  /** 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道) */
88
88
  getSupportedDiffVersion(): number;
89
+
90
+ // 更新流程决策层(cpp/update_flow_core,NATIVE_CHECKUPDATE_DESIGN §10):
91
+ // JSON 字符串进出,与决策层自身的边界一致。返回 undefined = 输入未通过
92
+ // 解析,编排器跳过本轮检测。
93
+ buildCheckRequestBody(inputJson: string): string | undefined;
94
+ orderEndpointCandidates(
95
+ endpointsJson: string,
96
+ randomSample: number,
97
+ ): string | undefined;
98
+ handleCheckResponse(
99
+ responseText: string,
100
+ identityJson: string,
101
+ afterDownload: string,
102
+ ): string | undefined;
89
103
  }
90
104
 
91
105
  export default NativeUpdateCore as unknown as NativePatchCoreBindings;
@@ -0,0 +1,21 @@
1
+ // Server-controlled identifiers are used as children of the update root. Keep
2
+ // validation in a dependency-free module so startup orchestration and storage
3
+ // code can share it without creating an import cycle.
4
+ export function isSafePathComponent(name: string): boolean {
5
+ return (
6
+ typeof name === 'string' &&
7
+ name.length > 0 &&
8
+ name !== '.' &&
9
+ name !== '..' &&
10
+ !name.includes('/') &&
11
+ !name.includes('\\') &&
12
+ !name.includes('\0')
13
+ );
14
+ }
15
+
16
+ export function assertSafePathComponent(name: string): string {
17
+ if (!isSafePathComponent(name)) {
18
+ throw Error(`Invalid path component: ${name}`);
19
+ }
20
+ return name;
21
+ }
@@ -8,6 +8,10 @@ import logger from './Logger';
8
8
  import NativePatchCore from './NativePatchCore';
9
9
  import { UpdateContext } from './UpdateContext';
10
10
  import { EventHub } from './EventHub';
11
+ import {
12
+ KEY_CONFIG,
13
+ KEY_RESP_CACHE,
14
+ } from './NativeCheckOrchestrator';
11
15
 
12
16
  const TAG = 'PushyTurboModule';
13
17
 
@@ -186,6 +190,24 @@ export class PushyTurboModule extends UITurboModule {
186
190
  this.context.setKv('uuid', uuid);
187
191
  }
188
192
 
193
+ // Provisioning for the native cold-start update check
194
+ // (NATIVE_CHECKUPDATE_DESIGN §10.1): the raw JSON persists as-is, parsed on
195
+ // read by the orchestrator; absent config = check disabled. Validated at
196
+ // write time — a corrupt config would otherwise silently disable the
197
+ // native check forever with no signal.
198
+ async syncNativeConfig(config: string): Promise<void> {
199
+ logger.debug(TAG, ',call syncNativeConfig');
200
+ JSON.parse(config);
201
+ this.context.setKv(KEY_CONFIG, config);
202
+ }
203
+
204
+ // 原生冷启动检测落盘的原始响应缓存,JS 侧新鲜期内直接复用免二次请求
205
+ // (NATIVE_CHECKUPDATE_DESIGN §10.3)。缺省空串,永不 reject。
206
+ async getNativeCheckCache(): Promise<string> {
207
+ logger.debug(TAG, ',call getNativeCheckCache');
208
+ return this.context.getKv(KEY_RESP_CACHE) ?? '';
209
+ }
210
+
189
211
  async reloadUpdate(options: { hash: string }): Promise<void> {
190
212
  logger.debug(TAG, ',call reloadUpdate');
191
213
  const hash = this.requireHash(options.hash, 'reloadUpdate');