react-native-update 10.56.0 → 10.57.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/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +102 -11
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateConfig.java +99 -0
- package/android/src/main/java/cn/reactnative/modules/update/NativeUpdateResult.java +51 -0
- package/android/src/main/java/cn/reactnative/modules/update/PushyNativeUpdate.java +127 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +35 -0
- package/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +1 -1
- package/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +121 -4
- package/harmony/pushy/src/main/ets/NativeUpdateConfig.ts +102 -0
- package/harmony/pushy/src/main/ets/NativeUpdateResult.ts +32 -0
- package/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +15 -0
- package/harmony/pushy/src/main/ets/PushyTurboModule.ts +1 -1
- package/harmony/pushy/src/main/ets/UpdateContext.ts +25 -0
- package/harmony/pushy.har +0 -0
- package/ios/RCTPushy/RCTPushy.h +24 -0
- package/ios/RCTPushy/RCTPushy.mm +191 -12
- package/ios/RCTPushy/RCTPushyNativeConfig.h +5 -0
- package/ios/RCTPushy/RCTPushyNativeConfig.mm +103 -0
- package/package.json +1 -1
- package/react-native-update.podspec +8 -1
- package/src/NativePushy.ts +3 -2
- package/src/client.ts +8 -0
- package/src/type.ts +7 -0
|
@@ -3,6 +3,8 @@ import deviceInfo from '@ohos.deviceInfo';
|
|
|
3
3
|
import logger from './Logger';
|
|
4
4
|
import NativePatchCore from './NativePatchCore';
|
|
5
5
|
import type { UpdateContext } from './UpdateContext';
|
|
6
|
+
import { NativeUpdateRound, nativeUpdateResult } from './NativeUpdateResult';
|
|
7
|
+
import type { NativeUpdateResult } from './NativeUpdateResult';
|
|
6
8
|
import { isSafePathComponent } from './PathUtils';
|
|
7
9
|
import { monotonicNowMs } from './MonotonicClock';
|
|
8
10
|
import {
|
|
@@ -106,6 +108,94 @@ interface RespCacheEntry {
|
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
let scheduled = false;
|
|
111
|
+
// The host and delayed check use one promise, including its settled result.
|
|
112
|
+
const hostRound = new NativeUpdateRound();
|
|
113
|
+
let scheduledContext: UpdateContext | undefined;
|
|
114
|
+
let scheduledRollback = '';
|
|
115
|
+
let roundGeneration = -1;
|
|
116
|
+
let roundConfigGeneration = -1;
|
|
117
|
+
let roundConfigJson: string | undefined;
|
|
118
|
+
let roundResult = nativeUpdateResult('failed', 'check_failed');
|
|
119
|
+
|
|
120
|
+
function startNativeRound(
|
|
121
|
+
context: UpdateContext,
|
|
122
|
+
launchRolledBackVersion: string,
|
|
123
|
+
): Promise<NativeUpdateResult> {
|
|
124
|
+
const preflight = configurationError(context);
|
|
125
|
+
if (preflight !== undefined) {
|
|
126
|
+
return Promise.resolve(preflight);
|
|
127
|
+
}
|
|
128
|
+
return hostRound.run(async () => {
|
|
129
|
+
try {
|
|
130
|
+
await runOnce(context, launchRolledBackVersion);
|
|
131
|
+
} catch (e) {
|
|
132
|
+
logger.error(TAG, `native check failed: ${getErrorMessage(e)}`);
|
|
133
|
+
roundResult = nativeUpdateResult('failed', 'internal_error');
|
|
134
|
+
}
|
|
135
|
+
return roundResult;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A preflight skip must not consume the process's only round: configuration
|
|
140
|
+
// may arrive after the delayed startup timer on a first-ever launch.
|
|
141
|
+
function configurationError(context: UpdateContext): NativeUpdateResult | undefined {
|
|
142
|
+
const json = context.getKv(KEY_CONFIG);
|
|
143
|
+
if (!json) {
|
|
144
|
+
return nativeUpdateResult('skipped', 'not_configured');
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const config = JSON.parse(json) as NativeConfig;
|
|
148
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
149
|
+
return nativeUpdateResult('failed', 'invalid_config');
|
|
150
|
+
}
|
|
151
|
+
if (config.disabled) {
|
|
152
|
+
return nativeUpdateResult('skipped', 'disabled');
|
|
153
|
+
}
|
|
154
|
+
if (typeof config.appKey !== 'string' || config.appKey.trim().length === 0) {
|
|
155
|
+
return nativeUpdateResult('failed', 'invalid_config');
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
return nativeUpdateResult('failed', 'invalid_config');
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function checkAndUpdateNative(
|
|
164
|
+
context: UpdateContext,
|
|
165
|
+
): Promise<NativeUpdateResult> {
|
|
166
|
+
if (scheduledContext !== context) {
|
|
167
|
+
return nativeUpdateResult('skipped', 'not_initialized');
|
|
168
|
+
}
|
|
169
|
+
const configJson = context.getKv(KEY_CONFIG);
|
|
170
|
+
if (!configJson) {
|
|
171
|
+
return nativeUpdateResult('skipped', 'not_configured');
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const config = JSON.parse(configJson) as NativeConfig;
|
|
175
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
176
|
+
return nativeUpdateResult('failed', 'invalid_config');
|
|
177
|
+
}
|
|
178
|
+
if (config.disabled) {
|
|
179
|
+
return nativeUpdateResult('skipped', 'disabled');
|
|
180
|
+
}
|
|
181
|
+
if (typeof config.appKey !== 'string' || !config.appKey) {
|
|
182
|
+
return nativeUpdateResult('failed', 'invalid_config');
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return nativeUpdateResult('failed', 'invalid_config');
|
|
186
|
+
}
|
|
187
|
+
const result = await startNativeRound(context, scheduledRollback);
|
|
188
|
+
if (roundConfigGeneration !== context.getNativeConfigGeneration()
|
|
189
|
+
|| configJson !== roundConfigJson || configJson !== context.getKv(KEY_CONFIG)) {
|
|
190
|
+
return nativeUpdateResult('cancelled', 'config_changed');
|
|
191
|
+
}
|
|
192
|
+
if (roundGeneration !== context.getResetGeneration()) {
|
|
193
|
+
return nativeUpdateResult('cancelled', 'reset');
|
|
194
|
+
}
|
|
195
|
+
// Do not let a caller mutate the cached result observed by later callers.
|
|
196
|
+
return nativeUpdateResult(result.status, result.reason, result.hash, result.activated);
|
|
197
|
+
}
|
|
198
|
+
|
|
109
199
|
// JS 在本进程内已拿到有效检查响应时对应的配置 JSON(markJsCheckCompleted)。
|
|
110
200
|
// 进程级:下次启动无信号,冷启动轮次照常运行。
|
|
111
201
|
let jsCompletedConfig: string | undefined;
|
|
@@ -131,6 +221,8 @@ export function scheduleNativeCheck(
|
|
|
131
221
|
return;
|
|
132
222
|
}
|
|
133
223
|
scheduled = true;
|
|
224
|
+
scheduledContext = context;
|
|
225
|
+
scheduledRollback = launchRolledBackVersion;
|
|
134
226
|
// 结果本来就是"下次启动生效",延迟几秒让开冷启动关键路径(§7 R5)——
|
|
135
227
|
// 除非上个进程死于轮中(残留标记),那时每一秒启动时间都要用来续传。
|
|
136
228
|
const delayMs = context.getKv(KEY_ROUND_INCOMPLETE) ? 0 : START_DELAY_MS;
|
|
@@ -142,7 +234,7 @@ export function scheduleNativeCheck(
|
|
|
142
234
|
);
|
|
143
235
|
return;
|
|
144
236
|
}
|
|
145
|
-
|
|
237
|
+
startNativeRound(context, launchRolledBackVersion).catch((e: Object) => {
|
|
146
238
|
// 救援路径自身绝不能把应用拖垮。
|
|
147
239
|
logger.error(TAG, `native check failed: ${getErrorMessage(e)}`);
|
|
148
240
|
});
|
|
@@ -156,22 +248,34 @@ async function runOnce(
|
|
|
156
248
|
// 在任何 IO 之前采样:resetToPackagedBundle 会递增它,本轮运行期间发生的
|
|
157
249
|
// reset 必须赢过本轮的决策。
|
|
158
250
|
const resetGeneration = context.getResetGeneration();
|
|
251
|
+
roundGeneration = resetGeneration;
|
|
252
|
+
roundConfigGeneration = context.getNativeConfigGeneration();
|
|
253
|
+
roundResult = nativeUpdateResult('failed', 'check_failed');
|
|
159
254
|
const configJson = context.getKv(KEY_CONFIG);
|
|
255
|
+
roundConfigJson = configJson;
|
|
160
256
|
if (!configJson) {
|
|
161
|
-
//
|
|
257
|
+
// No persisted configuration: report the rollout gate to native callers.
|
|
258
|
+
roundResult = nativeUpdateResult('skipped', 'not_configured');
|
|
162
259
|
return;
|
|
163
260
|
}
|
|
164
261
|
let config: NativeConfig;
|
|
165
262
|
try {
|
|
166
263
|
config = JSON.parse(configJson) as NativeConfig;
|
|
264
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
265
|
+
roundResult = nativeUpdateResult('failed', 'invalid_config');
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
167
268
|
} catch (e) {
|
|
269
|
+
roundResult = nativeUpdateResult('failed', 'invalid_config');
|
|
168
270
|
return;
|
|
169
271
|
}
|
|
170
272
|
if (config.disabled) {
|
|
273
|
+
roundResult = nativeUpdateResult('skipped', 'disabled');
|
|
171
274
|
return;
|
|
172
275
|
}
|
|
173
276
|
const appKey = config.appKey ?? '';
|
|
174
277
|
if (!appKey) {
|
|
278
|
+
roundResult = nativeUpdateResult('failed', 'invalid_config');
|
|
175
279
|
return;
|
|
176
280
|
}
|
|
177
281
|
// 从这里起本轮开始做真实工作:留下面包屑,死于轮中时下次启动零延迟续传。
|
|
@@ -267,6 +371,7 @@ async function runConfiguredRound(
|
|
|
267
371
|
};
|
|
268
372
|
const body = NativePatchCore.buildCheckRequestBody(JSON.stringify(input));
|
|
269
373
|
if (!body) {
|
|
374
|
+
roundResult = nativeUpdateResult('failed', 'invalid_request');
|
|
270
375
|
return;
|
|
271
376
|
}
|
|
272
377
|
|
|
@@ -285,23 +390,28 @@ async function runConfiguredRound(
|
|
|
285
390
|
config.afterDownload ?? '',
|
|
286
391
|
);
|
|
287
392
|
if (!decisionJson) {
|
|
393
|
+
roundResult = nativeUpdateResult('failed', 'invalid_response');
|
|
288
394
|
return;
|
|
289
395
|
}
|
|
290
396
|
const decision = JSON.parse(decisionJson) as Decision;
|
|
291
397
|
if (decision.action !== 'download') {
|
|
292
|
-
await context.commitNativeCheckResult(
|
|
398
|
+
const committed = await context.commitNativeCheckResult(
|
|
293
399
|
resetGeneration,
|
|
294
400
|
'',
|
|
295
401
|
'',
|
|
296
402
|
false,
|
|
297
403
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
|
|
298
404
|
);
|
|
405
|
+
roundResult = committed
|
|
406
|
+
? nativeUpdateResult('noUpdate', decision.reason ?? '')
|
|
407
|
+
: nativeUpdateResult('cancelled', 'reset');
|
|
299
408
|
logger.info(TAG, `nothing to do (${decision.reason ?? ''})`);
|
|
300
409
|
return;
|
|
301
410
|
}
|
|
302
411
|
const hash = decision.hash ?? '';
|
|
303
412
|
if (!isSafePathComponent(hash)) {
|
|
304
413
|
logger.warn(TAG, 'decision carries an unsafe hash, ignoring');
|
|
414
|
+
roundResult = nativeUpdateResult('failed', 'invalid_response');
|
|
305
415
|
return;
|
|
306
416
|
}
|
|
307
417
|
|
|
@@ -320,13 +430,16 @@ async function runConfiguredRound(
|
|
|
320
430
|
}
|
|
321
431
|
if (!downloaded) {
|
|
322
432
|
logger.warn(TAG, `all download attempts for ${hash} failed`);
|
|
323
|
-
await context.commitNativeCheckResult(
|
|
433
|
+
const committed = await context.commitNativeCheckResult(
|
|
324
434
|
resetGeneration,
|
|
325
435
|
'',
|
|
326
436
|
'',
|
|
327
437
|
false,
|
|
328
438
|
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds),
|
|
329
439
|
);
|
|
440
|
+
roundResult = committed
|
|
441
|
+
? nativeUpdateResult('failed', 'download_failed')
|
|
442
|
+
: nativeUpdateResult('cancelled', 'reset');
|
|
330
443
|
return;
|
|
331
444
|
}
|
|
332
445
|
|
|
@@ -368,6 +481,7 @@ async function runConfiguredRound(
|
|
|
368
481
|
);
|
|
369
482
|
} catch (e) {
|
|
370
483
|
logger.error(TAG, `commit failed: ${getErrorMessage(e)}`);
|
|
484
|
+
roundResult = nativeUpdateResult('failed', 'commit_failed');
|
|
371
485
|
return;
|
|
372
486
|
}
|
|
373
487
|
if (!committed) {
|
|
@@ -377,6 +491,9 @@ async function runConfiguredRound(
|
|
|
377
491
|
} else {
|
|
378
492
|
logger.info(TAG, `downloaded ${hash}, activation left to JS`);
|
|
379
493
|
}
|
|
494
|
+
roundResult = committed
|
|
495
|
+
? nativeUpdateResult('downloaded', '', hash, activate)
|
|
496
|
+
: nativeUpdateResult('cancelled', 'reset');
|
|
380
497
|
}
|
|
381
498
|
|
|
382
499
|
function buildResponseCacheJson(
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Options accepted by PushyFileJSBundleProvider.configure(). */
|
|
2
|
+
export interface NativeUpdateConfig {
|
|
3
|
+
appKey: string;
|
|
4
|
+
/** Omitted: Pushy's built-in endpoints. Custom endpoints never inherit discovery URLs. */
|
|
5
|
+
endpoints?: string[];
|
|
6
|
+
queryUrls?: string[];
|
|
7
|
+
/** Default: none. setNeedUpdate selects a downloaded bundle for the NEXT launch. */
|
|
8
|
+
afterDownload?: 'none' | 'setNeedUpdate';
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
/** Omit to use the installed application's version. */
|
|
11
|
+
packageVersion?: string;
|
|
12
|
+
/** Optional telemetry version strings; not required to check or install an update. */
|
|
13
|
+
rnu?: string;
|
|
14
|
+
rn?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PersistedNativeUpdateConfig {
|
|
18
|
+
appKey: string;
|
|
19
|
+
endpoints: string[];
|
|
20
|
+
queryUrls: string[];
|
|
21
|
+
afterDownload: string;
|
|
22
|
+
disabled: boolean;
|
|
23
|
+
packageVersion?: string;
|
|
24
|
+
rnu: string;
|
|
25
|
+
rn: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_ENDPOINTS: string[] = [
|
|
29
|
+
'https://update.react-native.cn/api',
|
|
30
|
+
'https://update.reactnative.cn/api',
|
|
31
|
+
];
|
|
32
|
+
const DEFAULT_QUERY_URLS: string[] = [
|
|
33
|
+
'https://gitee.com/sunnylqm/react-native-pushy/raw/master/endpoints.json',
|
|
34
|
+
'https://cdn.jsdelivr.net/gh/reactnativecn/react-native-update@master/endpoints.json',
|
|
35
|
+
];
|
|
36
|
+
const CONFIG_KEYS: string[] = [
|
|
37
|
+
'appKey', 'endpoints', 'queryUrls', 'afterDownload', 'disabled',
|
|
38
|
+
'packageVersion', 'rnu', 'rn',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function configString(value: string, name: string, allowEmpty: boolean): string {
|
|
42
|
+
if (typeof value !== 'string' || (!allowEmpty && value.trim().length === 0)) {
|
|
43
|
+
throw new Error(`Invalid native configuration: ${name} must be a string${allowEmpty ? '' : ' and must not be blank'}`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function configUrls(values: string[], name: string, base: boolean): string[] {
|
|
49
|
+
if (!Array.isArray(values) || (base && values.length === 0)) {
|
|
50
|
+
throw new Error(`Invalid native configuration: ${name} must be ${base ? 'a non-empty' : 'an'} array`);
|
|
51
|
+
}
|
|
52
|
+
const result: string[] = [];
|
|
53
|
+
for (const value of values) {
|
|
54
|
+
const url = configString(value, name, false).trim();
|
|
55
|
+
// Restrict the authority and scheme, without relying on browser URL globals
|
|
56
|
+
// unavailable in ArkTS. Platform networking performs the final URL parsing.
|
|
57
|
+
if (!/^https?:\/\/(\[[0-9a-fA-F:]+\]|[a-zA-Z0-9.-]+)(:[0-9]+)?([/?#]|$)/.test(url)
|
|
58
|
+
|| /\s|\\/.test(url) || (base && /[?#]/.test(url))) {
|
|
59
|
+
throw new Error(`Invalid native configuration: ${name} requires absolute HTTP(S) URLs without credentials${base ? ', queries or fragments' : ''}`);
|
|
60
|
+
}
|
|
61
|
+
const normalized = base ? url.replace(/\/+$/, '') : url;
|
|
62
|
+
if (!result.includes(normalized)) {
|
|
63
|
+
result.push(normalized);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Pure validation, before any native state is touched. Does not mutate options. */
|
|
70
|
+
export function normalizeNativeUpdateConfig(options: NativeUpdateConfig): string {
|
|
71
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
72
|
+
throw new Error('Invalid native configuration: expected an object');
|
|
73
|
+
}
|
|
74
|
+
for (const key of Object.keys(options)) {
|
|
75
|
+
if (!CONFIG_KEYS.includes(key)) {
|
|
76
|
+
throw new Error(`Invalid native configuration: unknown option ${key}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const appKey = configString(options.appKey, 'appKey', false);
|
|
80
|
+
const customEndpoints = options.endpoints !== undefined;
|
|
81
|
+
const endpoints = configUrls(customEndpoints ? options.endpoints! : DEFAULT_ENDPOINTS, 'endpoints', true);
|
|
82
|
+
const queryUrls = configUrls(
|
|
83
|
+
options.queryUrls !== undefined ? options.queryUrls : (customEndpoints ? [] : DEFAULT_QUERY_URLS),
|
|
84
|
+
'queryUrls', false,
|
|
85
|
+
);
|
|
86
|
+
const afterDownload = options.afterDownload === undefined ? 'none' : options.afterDownload;
|
|
87
|
+
if (afterDownload !== 'none' && afterDownload !== 'setNeedUpdate') {
|
|
88
|
+
throw new Error('Invalid native configuration: afterDownload must be none or setNeedUpdate');
|
|
89
|
+
}
|
|
90
|
+
if (options.disabled !== undefined && typeof options.disabled !== 'boolean') {
|
|
91
|
+
throw new Error('Invalid native configuration: disabled must be a boolean');
|
|
92
|
+
}
|
|
93
|
+
const result: PersistedNativeUpdateConfig = {
|
|
94
|
+
appKey, endpoints, queryUrls, afterDownload, disabled: options.disabled ?? false,
|
|
95
|
+
rnu: options.rnu === undefined ? '' : configString(options.rnu, 'rnu', true),
|
|
96
|
+
rn: options.rn === undefined ? '' : configString(options.rn, 'rn', true),
|
|
97
|
+
};
|
|
98
|
+
if (options.packageVersion !== undefined) {
|
|
99
|
+
result.packageVersion = configString(options.packageVersion, 'packageVersion', false);
|
|
100
|
+
}
|
|
101
|
+
return JSON.stringify(result);
|
|
102
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** A native round snapshot, not the current state of the running RN instance. */
|
|
2
|
+
export interface NativeUpdateResult {
|
|
3
|
+
/** skipped, noUpdate, downloaded, failed, or cancelled. */
|
|
4
|
+
status: string;
|
|
5
|
+
reason: string;
|
|
6
|
+
hash: string;
|
|
7
|
+
/** The version was selected for the next launch; no reload is performed. */
|
|
8
|
+
activated: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function nativeUpdateResult(
|
|
12
|
+
status: string,
|
|
13
|
+
reason: string = '',
|
|
14
|
+
hash: string = '',
|
|
15
|
+
activated: boolean = false,
|
|
16
|
+
): NativeUpdateResult {
|
|
17
|
+
return { status, reason, hash, activated };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Internal gate shared by the delayed check and the native host API. */
|
|
21
|
+
export class NativeUpdateRound {
|
|
22
|
+
private task: Promise<NativeUpdateResult> | undefined;
|
|
23
|
+
|
|
24
|
+
run(operation: () => Promise<NativeUpdateResult>): Promise<NativeUpdateResult> {
|
|
25
|
+
if (this.task !== undefined) {
|
|
26
|
+
return this.task;
|
|
27
|
+
}
|
|
28
|
+
// Defer invocation until the promise is stored, including reentrant callers.
|
|
29
|
+
this.task = Promise.resolve().then(operation);
|
|
30
|
+
return this.task;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
import common from '@ohos.app.ability.common';
|
|
7
7
|
import fs from '@ohos.file.fs';
|
|
8
8
|
import { UpdateContext } from './UpdateContext';
|
|
9
|
+
import { checkAndUpdateNative } from './NativeCheckOrchestrator';
|
|
10
|
+
import type { NativeUpdateResult } from './NativeUpdateResult';
|
|
11
|
+
import { normalizeNativeUpdateConfig } from './NativeUpdateConfig';
|
|
12
|
+
import type { NativeUpdateConfig } from './NativeUpdateConfig';
|
|
9
13
|
|
|
10
14
|
export class PushyFileJSBundleProvider extends JSBundleProvider {
|
|
11
15
|
private updateContext: UpdateContext;
|
|
@@ -43,6 +47,17 @@ export class PushyFileJSBundleProvider extends JSBundleProvider {
|
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
/** Configure before normal bundle resolution; no JS or network work is required. */
|
|
51
|
+
async configure(options: NativeUpdateConfig): Promise<void> {
|
|
52
|
+
const config = normalizeNativeUpdateConfig(options);
|
|
53
|
+
await this.updateContext.setNativeConfig(config);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Call after the host's real bundle resolution; never resolves it again. */
|
|
57
|
+
checkAndUpdate(): Promise<NativeUpdateResult> {
|
|
58
|
+
return checkAndUpdateNative(this.updateContext);
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
getAppKeys(): string[] {
|
|
47
62
|
return [];
|
|
48
63
|
}
|
|
@@ -273,7 +273,7 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
273
273
|
);
|
|
274
274
|
}
|
|
275
275
|
try {
|
|
276
|
-
await this.context.
|
|
276
|
+
await this.context.setNativeConfig(config);
|
|
277
277
|
} catch (error) {
|
|
278
278
|
throw toUpdateError(error, ERROR_FILE_OPERATION_FAILED);
|
|
279
279
|
}
|
|
@@ -12,7 +12,9 @@ import { bundleManager } from '@kit.AbilityKit';
|
|
|
12
12
|
import { util } from '@kit.ArkTS';
|
|
13
13
|
import logger from './Logger';
|
|
14
14
|
import {
|
|
15
|
+
KEY_CONFIG,
|
|
15
16
|
KEY_RESP_CACHE,
|
|
17
|
+
markJsCheckCompleted,
|
|
16
18
|
scheduleNativeCheck,
|
|
17
19
|
} from './NativeCheckOrchestrator';
|
|
18
20
|
import NativePatchCore, {
|
|
@@ -458,6 +460,29 @@ export class UpdateContext {
|
|
|
458
460
|
}
|
|
459
461
|
|
|
460
462
|
/** 写入并落盘;flushSync 不可用时以 flush() 的结果拒绝。 */
|
|
463
|
+
/** Shared JS/native configuration writer; all mutations precede the first await. */
|
|
464
|
+
private static nativeConfigGeneration: number = 0;
|
|
465
|
+
|
|
466
|
+
public getNativeConfigGeneration(): number {
|
|
467
|
+
return UpdateContext.nativeConfigGeneration;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
public setNativeConfig(config: string): Promise<void> {
|
|
471
|
+
if (!this.getKv('uuid')) {
|
|
472
|
+
this.preferences.putSync('uuid', util.generateRandomUUID());
|
|
473
|
+
}
|
|
474
|
+
if (this.getKv(KEY_CONFIG) !== config) {
|
|
475
|
+
// Also guards A -> B -> A replacements and late download commits.
|
|
476
|
+
UpdateContext.resetGeneration += 1;
|
|
477
|
+
UpdateContext.nativeConfigGeneration += 1;
|
|
478
|
+
this.preferences.putSync(KEY_CONFIG, config);
|
|
479
|
+
this.preferences.deleteSync(KEY_RESP_CACHE);
|
|
480
|
+
markJsCheckCompleted('');
|
|
481
|
+
}
|
|
482
|
+
// Flush even an equal value so a retry after a storage error can succeed.
|
|
483
|
+
return this.flushPreferences('configure native update');
|
|
484
|
+
}
|
|
485
|
+
|
|
461
486
|
public setKv(key: string, value: string): Promise<void> {
|
|
462
487
|
this.preferences.putSync(key, value);
|
|
463
488
|
return this.flushPreferences(`set key ${key}`);
|
package/harmony/pushy.har
CHANGED
|
Binary file
|
package/ios/RCTPushy/RCTPushy.h
CHANGED
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
#import <React/RCTBridgeModule.h>
|
|
2
2
|
#import <React/RCTEventEmitter.h>
|
|
3
3
|
|
|
4
|
+
typedef void (^RCTPushyNativeConfigurationCompletion)(NSError * _Nullable error);
|
|
5
|
+
|
|
6
|
+
typedef void (^RCTPushyNativeUpdateCompletion)(NSDictionary<NSString *, id> * _Nonnull result);
|
|
4
7
|
|
|
5
8
|
@interface RCTPushy : RCTEventEmitter<RCTBridgeModule>
|
|
6
9
|
|
|
7
10
|
+ (NSURL *)bundleURL;
|
|
8
11
|
|
|
12
|
+
/** Validate and persist native options without JS, network work or bundle resolution.
|
|
13
|
+
* Completion is on the main queue; nil error means success. Call before the
|
|
14
|
+
* normal launch bundle resolution for first-install native-only provisioning.
|
|
15
|
+
*/
|
|
16
|
+
+ (void)configure:(NSDictionary<NSString *, id> * _Nonnull)options
|
|
17
|
+
completion:(RCTPushyNativeConfigurationCompletion _Nullable)completion
|
|
18
|
+
NS_SWIFT_NAME(configure(_:completion:));
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Start, join, or reuse this process's native update round. Call after the
|
|
22
|
+
* host's real bundleURL resolution; this method never resolves the bundle
|
|
23
|
+
* again, reloads React Native, or displays UI. Configuration is the one
|
|
24
|
+
* persisted by the JS SDK. The optional completion runs on the main queue.
|
|
25
|
+
*
|
|
26
|
+
* Result keys: status (skipped/noUpdate/downloaded/failed/cancelled), reason,
|
|
27
|
+
* hash (empty when nothing was installed), activated (selected for NEXT
|
|
28
|
+
* launch, not a reload of the running instance).
|
|
29
|
+
*/
|
|
30
|
+
+ (void)checkAndUpdateWithCompletion:(RCTPushyNativeUpdateCompletion _Nullable)completion
|
|
31
|
+
NS_SWIFT_NAME(checkAndUpdate(completion:));
|
|
32
|
+
|
|
9
33
|
@end
|