react-native-update 10.42.3 → 10.42.5
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/harmony/pushy/src/main/ets/PushyTurboModule.ts +3 -31
- package/harmony/pushy/src/main/ets/UpdateContext.ts +89 -6
- package/harmony/pushy.har +0 -0
- package/package.json +1 -1
- package/src/client.ts +79 -56
- package/src/globals.d.ts +2 -0
- package/src/type.ts +4 -0
- package/src/utils.ts +5 -0
|
@@ -7,7 +7,6 @@ import { bundleManager } from '@kit.AbilityKit';
|
|
|
7
7
|
import logger from './Logger';
|
|
8
8
|
import { UpdateContext } from './UpdateContext';
|
|
9
9
|
import { EventHub } from './EventHub';
|
|
10
|
-
import { util } from '@kit.ArkTS';
|
|
11
10
|
|
|
12
11
|
const TAG = 'PushyTurboModule';
|
|
13
12
|
|
|
@@ -47,33 +46,6 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
47
46
|
return bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
private getPackageVersion(): string {
|
|
51
|
-
try {
|
|
52
|
-
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
|
|
53
|
-
this.getBundleFlags(),
|
|
54
|
-
);
|
|
55
|
-
return bundleInfo?.versionName || 'Unknown';
|
|
56
|
-
} catch (error) {
|
|
57
|
-
console.error('Failed to get bundle info:', error);
|
|
58
|
-
return '';
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private getBuildTime(): string {
|
|
63
|
-
try {
|
|
64
|
-
const content = this.mUiCtx.resourceManager.getRawFileContentSync(
|
|
65
|
-
'meta.json',
|
|
66
|
-
);
|
|
67
|
-
const metaData = JSON.parse(
|
|
68
|
-
new util.TextDecoder().decodeToString(content),
|
|
69
|
-
) as Record<string, string | number | boolean | null | undefined>;
|
|
70
|
-
if (metaData.pushy_build_time) {
|
|
71
|
-
return String(metaData.pushy_build_time);
|
|
72
|
-
}
|
|
73
|
-
} catch {}
|
|
74
|
-
return '';
|
|
75
|
-
}
|
|
76
|
-
|
|
77
49
|
private requireHash(hash: string, methodName: string): string {
|
|
78
50
|
if (!hash) {
|
|
79
51
|
throw Error(`${methodName}: empty hash`);
|
|
@@ -95,15 +67,15 @@ export class PushyTurboModule extends UITurboModule {
|
|
|
95
67
|
|
|
96
68
|
getConstants(): Object {
|
|
97
69
|
logger.debug(TAG, ',call getConstants');
|
|
98
|
-
const packageVersion = this.getPackageVersion();
|
|
99
|
-
const buildTime = this.getBuildTime();
|
|
70
|
+
const packageVersion = this.context.getPackageVersion();
|
|
71
|
+
const buildTime = this.context.getBuildTime();
|
|
100
72
|
this.context.syncStateWithBinaryVersion(packageVersion, buildTime);
|
|
101
73
|
|
|
102
74
|
const currentVersion = this.context.getCurrentVersion();
|
|
103
75
|
const currentVersionInfo = currentVersion
|
|
104
76
|
? this.context.getKv(`hash_${currentVersion}`)
|
|
105
77
|
: '';
|
|
106
|
-
const isFirstTime = this.context.
|
|
78
|
+
const isFirstTime = this.context.consumeFirstLoadMarker();
|
|
107
79
|
const rolledBackVersion = this.context.rolledBackVersion();
|
|
108
80
|
const uuid = this.context.getKv('uuid');
|
|
109
81
|
const isUsingBundleUrl = this.context.getIsUsingBundleUrl();
|
|
@@ -3,6 +3,8 @@ import fileIo from '@ohos.file.fs';
|
|
|
3
3
|
import { DownloadTask } from './DownloadTask';
|
|
4
4
|
import common from '@ohos.app.ability.common';
|
|
5
5
|
import { DownloadTaskParams } from './DownloadTaskParams';
|
|
6
|
+
import { bundleManager } from '@kit.AbilityKit';
|
|
7
|
+
import { util } from '@kit.ArkTS';
|
|
6
8
|
import NativePatchCore, {
|
|
7
9
|
STATE_OP_CLEAR_FIRST_TIME,
|
|
8
10
|
STATE_OP_CLEAR_ROLLBACK_MARK,
|
|
@@ -13,12 +15,17 @@ import NativePatchCore, {
|
|
|
13
15
|
StateCoreResult,
|
|
14
16
|
} from './NativePatchCore';
|
|
15
17
|
|
|
18
|
+
type FlushablePreferences = preferences.Preferences & {
|
|
19
|
+
flushSync?: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
16
22
|
export class UpdateContext {
|
|
17
23
|
private context: common.UIAbilityContext;
|
|
18
24
|
private rootDir: string;
|
|
19
25
|
private preferences!: preferences.Preferences;
|
|
20
26
|
private static DEBUG: boolean = false;
|
|
21
27
|
private static isUsingBundleUrl: boolean = false;
|
|
28
|
+
private static ignoreRollback: boolean = false;
|
|
22
29
|
|
|
23
30
|
constructor(context: common.UIAbilityContext) {
|
|
24
31
|
this.context = context;
|
|
@@ -32,6 +39,10 @@ export class UpdateContext {
|
|
|
32
39
|
console.error('Failed to create root directory:', e);
|
|
33
40
|
}
|
|
34
41
|
this.initPreferences();
|
|
42
|
+
this.syncStateWithBinaryVersion(
|
|
43
|
+
this.getPackageVersion(),
|
|
44
|
+
this.getBuildTime(),
|
|
45
|
+
);
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
private initPreferences() {
|
|
@@ -44,6 +55,36 @@ export class UpdateContext {
|
|
|
44
55
|
}
|
|
45
56
|
}
|
|
46
57
|
|
|
58
|
+
private getBundleFlags(): bundleManager.BundleFlag {
|
|
59
|
+
return bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_REQUESTED_PERMISSION;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public getPackageVersion(): string {
|
|
63
|
+
try {
|
|
64
|
+
const bundleInfo = bundleManager.getBundleInfoForSelfSync(
|
|
65
|
+
this.getBundleFlags(),
|
|
66
|
+
);
|
|
67
|
+
return bundleInfo?.versionName || 'Unknown';
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.error('Failed to get bundle info:', error);
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public getBuildTime(): string {
|
|
75
|
+
try {
|
|
76
|
+
const content =
|
|
77
|
+
this.context.resourceManager.getRawFileContentSync('meta.json');
|
|
78
|
+
const metaData = JSON.parse(
|
|
79
|
+
new util.TextDecoder().decodeToString(content),
|
|
80
|
+
) as Record<string, string | number | boolean | null | undefined>;
|
|
81
|
+
if (metaData.pushy_build_time) {
|
|
82
|
+
return String(metaData.pushy_build_time);
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
|
|
47
88
|
private readString(key: string): string {
|
|
48
89
|
const value = this.preferences.getSync(key, '') as
|
|
49
90
|
| string
|
|
@@ -87,6 +128,20 @@ export class UpdateContext {
|
|
|
87
128
|
return defaultValue;
|
|
88
129
|
}
|
|
89
130
|
|
|
131
|
+
private flushPreferences(reason: string): void {
|
|
132
|
+
try {
|
|
133
|
+
const flushablePreferences = this.preferences as FlushablePreferences;
|
|
134
|
+
if (typeof flushablePreferences.flushSync === 'function') {
|
|
135
|
+
flushablePreferences.flushSync();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
throw Error('preferences.flushSync is not available');
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error(`Failed to flush preferences for ${reason}:`, error);
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
90
145
|
private putNullableString(key: string, value?: string): void {
|
|
91
146
|
if (value) {
|
|
92
147
|
this.preferences.putSync(key, value);
|
|
@@ -127,6 +182,8 @@ export class UpdateContext {
|
|
|
127
182
|
clearExisting?: boolean;
|
|
128
183
|
removeStaleHash?: boolean;
|
|
129
184
|
cleanUp?: boolean;
|
|
185
|
+
markFirstLoadMarker?: boolean;
|
|
186
|
+
clearFirstLoadMarker?: boolean;
|
|
130
187
|
} = {},
|
|
131
188
|
): void {
|
|
132
189
|
if (options.clearExisting) {
|
|
@@ -136,7 +193,13 @@ export class UpdateContext {
|
|
|
136
193
|
if (options.removeStaleHash && state.staleVersionToDelete) {
|
|
137
194
|
this.preferences.deleteSync(`hash_${state.staleVersionToDelete}`);
|
|
138
195
|
}
|
|
139
|
-
|
|
196
|
+
if (options.markFirstLoadMarker) {
|
|
197
|
+
this.preferences.putSync('firstLoadMarked', 'true');
|
|
198
|
+
}
|
|
199
|
+
if (options.clearFirstLoadMarker) {
|
|
200
|
+
this.preferences.deleteSync('firstLoadMarked');
|
|
201
|
+
}
|
|
202
|
+
this.flushPreferences('persist state');
|
|
140
203
|
if (options.cleanUp) {
|
|
141
204
|
this.cleanUp();
|
|
142
205
|
}
|
|
@@ -148,6 +211,7 @@ export class UpdateContext {
|
|
|
148
211
|
options: {
|
|
149
212
|
removeStaleHash?: boolean;
|
|
150
213
|
cleanUp?: boolean;
|
|
214
|
+
clearFirstLoadMarker?: boolean;
|
|
151
215
|
} = {},
|
|
152
216
|
): StateCoreResult {
|
|
153
217
|
const nextState = NativePatchCore.runStateCore(
|
|
@@ -190,13 +254,14 @@ export class UpdateContext {
|
|
|
190
254
|
return;
|
|
191
255
|
}
|
|
192
256
|
|
|
257
|
+
UpdateContext.ignoreRollback = false;
|
|
193
258
|
this.cleanUp();
|
|
194
259
|
this.persistState(nextState, { clearExisting: true });
|
|
195
260
|
}
|
|
196
261
|
|
|
197
262
|
public setKv(key: string, value: string): void {
|
|
198
263
|
this.preferences.putSync(key, value);
|
|
199
|
-
this.
|
|
264
|
+
this.flushPreferences(`set key ${key}`);
|
|
200
265
|
}
|
|
201
266
|
|
|
202
267
|
public getKv(key: string): string {
|
|
@@ -223,7 +288,10 @@ export class UpdateContext {
|
|
|
223
288
|
}
|
|
224
289
|
|
|
225
290
|
public clearFirstTime(): void {
|
|
226
|
-
this.runStateOperation(STATE_OP_CLEAR_FIRST_TIME, '', {
|
|
291
|
+
this.runStateOperation(STATE_OP_CLEAR_FIRST_TIME, '', {
|
|
292
|
+
cleanUp: true,
|
|
293
|
+
clearFirstLoadMarker: true,
|
|
294
|
+
});
|
|
227
295
|
}
|
|
228
296
|
|
|
229
297
|
public clearRollbackMark(): void {
|
|
@@ -306,23 +374,38 @@ export class UpdateContext {
|
|
|
306
374
|
}
|
|
307
375
|
|
|
308
376
|
this.runStateOperation(STATE_OP_SWITCH_VERSION, hash);
|
|
377
|
+
UpdateContext.ignoreRollback = false;
|
|
309
378
|
} catch (e) {
|
|
310
379
|
console.error('Failed to switch version:', e);
|
|
311
380
|
throw e;
|
|
312
381
|
}
|
|
313
382
|
}
|
|
314
383
|
|
|
384
|
+
public consumeFirstLoadMarker(): boolean {
|
|
385
|
+
const marked = this.readString('firstLoadMarked') === 'true';
|
|
386
|
+
if (marked) {
|
|
387
|
+
this.preferences.deleteSync('firstLoadMarked');
|
|
388
|
+
this.flushPreferences('clear first load marker');
|
|
389
|
+
}
|
|
390
|
+
return marked;
|
|
391
|
+
}
|
|
392
|
+
|
|
315
393
|
public getBundleUrl() {
|
|
316
394
|
UpdateContext.isUsingBundleUrl = true;
|
|
317
395
|
const launchState = NativePatchCore.runStateCore(
|
|
318
396
|
STATE_OP_RESOLVE_LAUNCH,
|
|
319
397
|
this.getStateSnapshot(),
|
|
320
398
|
'',
|
|
321
|
-
|
|
322
|
-
|
|
399
|
+
UpdateContext.ignoreRollback,
|
|
400
|
+
true,
|
|
323
401
|
);
|
|
324
402
|
if (launchState.didRollback || launchState.consumedFirstTime) {
|
|
325
|
-
this.persistState(launchState
|
|
403
|
+
this.persistState(launchState, {
|
|
404
|
+
markFirstLoadMarker: launchState.consumedFirstTime,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (launchState.consumedFirstTime) {
|
|
408
|
+
UpdateContext.ignoreRollback = true;
|
|
326
409
|
}
|
|
327
410
|
|
|
328
411
|
let version = launchState.loadVersion || '';
|
package/harmony/pushy.har
CHANGED
|
Binary file
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from './type';
|
|
29
29
|
import {
|
|
30
30
|
assertWeb,
|
|
31
|
+
computeProgress,
|
|
31
32
|
DEFAULT_FETCH_TIMEOUT_MS,
|
|
32
33
|
emptyObj,
|
|
33
34
|
fetchWithTimeout,
|
|
@@ -491,13 +492,19 @@ export class Pushy {
|
|
|
491
492
|
}
|
|
492
493
|
const patchStartTime = Date.now();
|
|
493
494
|
if (onDownloadProgress) {
|
|
495
|
+
const wrapProgress = (data: ProgressData) => {
|
|
496
|
+
onDownloadProgress({
|
|
497
|
+
...data,
|
|
498
|
+
progress: computeProgress(data.received, data.total),
|
|
499
|
+
});
|
|
500
|
+
};
|
|
494
501
|
// @ts-expect-error harmony not in existing platforms
|
|
495
502
|
if (Platform.OS === 'harmony') {
|
|
496
503
|
sharedState.progressHandlers[hash] = DeviceEventEmitter.addListener(
|
|
497
504
|
'RCTPushyDownloadProgress',
|
|
498
|
-
progressData => {
|
|
505
|
+
(progressData: ProgressData) => {
|
|
499
506
|
if (progressData.hash === hash) {
|
|
500
|
-
|
|
507
|
+
wrapProgress(progressData);
|
|
501
508
|
}
|
|
502
509
|
},
|
|
503
510
|
);
|
|
@@ -505,54 +512,47 @@ export class Pushy {
|
|
|
505
512
|
sharedState.progressHandlers[hash] =
|
|
506
513
|
pushyNativeEventEmitter.addListener(
|
|
507
514
|
'RCTPushyDownloadProgress',
|
|
508
|
-
progressData => {
|
|
515
|
+
(progressData: ProgressData) => {
|
|
509
516
|
if (progressData.hash === hash) {
|
|
510
|
-
|
|
517
|
+
wrapProgress(progressData);
|
|
511
518
|
}
|
|
512
519
|
},
|
|
513
520
|
);
|
|
514
521
|
}
|
|
515
522
|
}
|
|
523
|
+
const maxRetries = Math.max(0, Math.floor(this.options.maxRetries ?? 3));
|
|
516
524
|
let succeeded = '';
|
|
517
|
-
this.report({
|
|
518
|
-
type: 'downloading',
|
|
519
|
-
data: {
|
|
520
|
-
newVersion: hash,
|
|
521
|
-
},
|
|
522
|
-
});
|
|
523
525
|
let lastError: any;
|
|
524
526
|
const errorMessages: string[] = [];
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
succeeded = 'diff';
|
|
535
|
-
} catch (e: any) {
|
|
536
|
-
const errorMessage = this.t('error_diff_failed', {
|
|
537
|
-
message: e.message,
|
|
538
|
-
});
|
|
539
|
-
errorMessages.push(errorMessage);
|
|
540
|
-
lastError = Error(errorMessage);
|
|
541
|
-
log(errorMessage);
|
|
527
|
+
|
|
528
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
529
|
+
if (attempt > 0) {
|
|
530
|
+
const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 10000);
|
|
531
|
+
log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
|
|
532
|
+
await new Promise(r => setTimeout(r, backoffMs));
|
|
533
|
+
errorMessages.length = 0;
|
|
534
|
+
lastError = undefined;
|
|
535
|
+
succeeded = '';
|
|
542
536
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
537
|
+
this.report({
|
|
538
|
+
type: 'downloading',
|
|
539
|
+
data: {
|
|
540
|
+
newVersion: hash,
|
|
541
|
+
attempt,
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
const diffUrl = await testUrls(joinUrls(paths, diff));
|
|
545
|
+
if (diffUrl && !__DEV__) {
|
|
546
|
+
log('downloading diff');
|
|
548
547
|
try {
|
|
549
|
-
await PushyModule.
|
|
550
|
-
updateUrl:
|
|
548
|
+
await PushyModule.downloadPatchFromPpk({
|
|
549
|
+
updateUrl: diffUrl,
|
|
551
550
|
hash,
|
|
551
|
+
originHash: currentVersion,
|
|
552
552
|
});
|
|
553
|
-
succeeded = '
|
|
553
|
+
succeeded = 'diff';
|
|
554
554
|
} catch (e: any) {
|
|
555
|
-
const errorMessage = this.t('
|
|
555
|
+
const errorMessage = this.t('error_diff_failed', {
|
|
556
556
|
message: e.message,
|
|
557
557
|
});
|
|
558
558
|
errorMessages.push(errorMessage);
|
|
@@ -560,28 +560,51 @@ export class Pushy {
|
|
|
560
560
|
log(errorMessage);
|
|
561
561
|
}
|
|
562
562
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
563
|
+
if (!succeeded) {
|
|
564
|
+
const pdiffUrl = await testUrls(joinUrls(paths, pdiff));
|
|
565
|
+
if (pdiffUrl && !__DEV__) {
|
|
566
|
+
log('downloading pdiff');
|
|
567
|
+
try {
|
|
568
|
+
await PushyModule.downloadPatchFromPackage({
|
|
569
|
+
updateUrl: pdiffUrl,
|
|
570
|
+
hash,
|
|
571
|
+
});
|
|
572
|
+
succeeded = 'pdiff';
|
|
573
|
+
} catch (e: any) {
|
|
574
|
+
const errorMessage = this.t('error_pdiff_failed', {
|
|
575
|
+
message: e.message,
|
|
576
|
+
});
|
|
577
|
+
errorMessages.push(errorMessage);
|
|
578
|
+
lastError = Error(errorMessage);
|
|
579
|
+
log(errorMessage);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (!succeeded) {
|
|
584
|
+
const fullUrl = await testUrls(joinUrls(paths, full));
|
|
585
|
+
if (fullUrl) {
|
|
586
|
+
log('downloading full patch');
|
|
587
|
+
try {
|
|
588
|
+
await PushyModule.downloadFullUpdate({
|
|
589
|
+
updateUrl: fullUrl,
|
|
590
|
+
hash,
|
|
591
|
+
});
|
|
592
|
+
succeeded = 'full';
|
|
593
|
+
} catch (e: any) {
|
|
594
|
+
const errorMessage = this.t('error_full_patch_failed', {
|
|
595
|
+
message: e.message,
|
|
596
|
+
});
|
|
597
|
+
errorMessages.push(errorMessage);
|
|
598
|
+
lastError = Error(errorMessage);
|
|
599
|
+
log(errorMessage);
|
|
600
|
+
}
|
|
601
|
+
} else if (__DEV__) {
|
|
602
|
+
log(this.t('dev_incremental_update_disabled'));
|
|
573
603
|
succeeded = 'full';
|
|
574
|
-
} catch (e: any) {
|
|
575
|
-
const errorMessage = this.t('error_full_patch_failed', {
|
|
576
|
-
message: e.message,
|
|
577
|
-
});
|
|
578
|
-
errorMessages.push(errorMessage);
|
|
579
|
-
lastError = Error(errorMessage);
|
|
580
|
-
log(errorMessage);
|
|
581
604
|
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
605
|
+
}
|
|
606
|
+
if (succeeded) {
|
|
607
|
+
break;
|
|
585
608
|
}
|
|
586
609
|
}
|
|
587
610
|
if (sharedState.progressHandlers[hash]) {
|
package/src/globals.d.ts
ADDED
package/src/type.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface ProgressData {
|
|
|
33
33
|
hash: string;
|
|
34
34
|
received: number;
|
|
35
35
|
total: number;
|
|
36
|
+
/** Download progress percentage (0-100), computed from received / total and clamped to range. Only populated in downloadUpdate callbacks. */
|
|
37
|
+
progress?: number;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
// 用于描述一次检查结束后的最终状态,便于业务侧感知成功、跳过或失败
|
|
@@ -119,6 +121,8 @@ export interface ClientOptions {
|
|
|
119
121
|
) => Promise<boolean | void> | boolean | void;
|
|
120
122
|
onPackageExpired?: (info: CheckResult) => Promise<boolean> | boolean;
|
|
121
123
|
overridePackageVersion?: string;
|
|
124
|
+
/** Maximum number of retry attempts for failed downloads (default: 3) */
|
|
125
|
+
maxRetries?: number;
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
export interface UpdateTestPayload {
|
package/src/utils.ts
CHANGED
|
@@ -108,6 +108,11 @@ export const assertWeb = () => {
|
|
|
108
108
|
return true;
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
+
export const computeProgress = (received: number, total: number): number =>
|
|
112
|
+
total > 0
|
|
113
|
+
? Math.min(100, Math.max(0, Math.floor((received / total) * 100)))
|
|
114
|
+
: 0;
|
|
115
|
+
|
|
111
116
|
export const fetchWithTimeout = (
|
|
112
117
|
url: string,
|
|
113
118
|
params: Parameters<typeof fetch>[1],
|