Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private UpdateContext(Context context) {
// Execute cleanUp before clearing SharedPreferences to avoid race condition
cleanUp(DownloadTaskParams.DEFAULT_MAX_AGE_DAYS);
SharedPreferences.Editor editor = this.sp.edit();
removeVersionScopedKeys(editor);
removeKeys(editor, VersionScopedKeys.clearedOnUpgrade(sp.getAll().keySet()));
applyState(editor, nextState);
persistEditor(editor, "sync state with binary version");
clearNativeCheckCache();
Expand Down Expand Up @@ -441,8 +441,8 @@ private static void putNullableString(
// Not editor.clear(): that also wiped the uuid (re-bucketing every gray
// release on each app upgrade) and the native config (no crash hold and
// no cold-start round until JS ran again). See VersionScopedKeys.
private void removeVersionScopedKeys(SharedPreferences.Editor editor) {
for (String key : VersionScopedKeys.select(sp.getAll().keySet())) {
private void removeKeys(SharedPreferences.Editor editor, java.util.List<String> keys) {
for (String key : keys) {
editor.remove(key);
}
}
Expand Down Expand Up @@ -656,7 +656,7 @@ private void resetToPackagedBundleLocked() {
throw new IllegalStateException("Failed to persist state for reset check cache");
}
SharedPreferences.Editor editor = sp.edit();
removeVersionScopedKeys(editor);
removeKeys(editor, VersionScopedKeys.clearedOnReset(sp.getAll().keySet()));
applyState(editor, resetState);
persistEditorOrThrow(editor, "reset to packaged bundle");
ignoreRollback = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,56 @@

/**
* The keys of the main state file that describe installed versions rather
* than the install itself: per-version info (hash_*), the first-load marker,
* the native round's incomplete breadcrumb and the response cache's legacy
* location. A binary upgrade or a reset to the packaged bundle drops exactly
* these (plus the state fields applyState rewrites). Everything else — the
* client uuid that buckets gray releases, the persisted native config the
* cold-start round and crash hold run from, the bundle hash cache keyed by
* package version — belongs to the install and survives, as on iOS and
* Harmony.
* than the install itself. What a binary upgrade or a reset to the packaged
* bundle drops (besides the state fields applyState rewrites) is the same on
* every platform and generated from scripts/contract.ts (storedItems); the
* install's own keys — the client uuid that buckets gray releases, the
* persisted native config the cold-start round and crash hold run from, the
* round breadcrumb, the bundle hash cache keyed by package version — survive.
*/
final class VersionScopedKeys {
static final String HASH_INFO_PREFIX = "hash_";

// <generated:lifecycle> (scripts/contract.ts; bun scripts/generate-contract.ts)
// What a reset to the packaged bundle and a binary upgrade delete besides
// the state fields (scripts/contract.ts storedItems; every other item survives
// both: the uuid, the native config, the round breadcrumb, the bundle hash
// cache).
static final String[] RESET_KEYS = {UpdateContext.KEY_FIRST_LOAD_MARKED, SyncCoordinator.KEY_RESP_CACHE};
static final String[] RESET_PREFIXES = {HASH_INFO_PREFIX};
static final String[] UPGRADE_KEYS = {UpdateContext.KEY_FIRST_LOAD_MARKED, SyncCoordinator.KEY_RESP_CACHE};
static final String[] UPGRADE_PREFIXES = {HASH_INFO_PREFIX};
// </generated:lifecycle>

private VersionScopedKeys() {
}

static List<String> select(Collection<String> keys) {
/** The keys a reset to the packaged bundle deletes. */
static List<String> clearedOnReset(Collection<String> keys) {
return select(keys, RESET_KEYS, RESET_PREFIXES);
}

/** The keys a binary upgrade deletes. */
static List<String> clearedOnUpgrade(Collection<String> keys) {
return select(keys, UPGRADE_KEYS, UPGRADE_PREFIXES);
}

private static List<String> select(
Collection<String> keys, String[] exact, String[] prefixes
) {
List<String> doomed = new ArrayList<>();
for (String key : keys) {
if (key == null) {
continue;
}
if (key.startsWith(HASH_INFO_PREFIX)
|| key.equals(UpdateContext.KEY_FIRST_LOAD_MARKED)
|| key.equals(SyncCoordinator.KEY_ROUND_INCOMPLETE)
|| key.equals(SyncCoordinator.KEY_RESP_CACHE)) {
boolean match = false;
for (String name : exact) {
match |= key.equals(name);
}
for (String prefix : prefixes) {
match |= key.startsWith(prefix);
}
if (match) {
doomed.add(key);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,33 @@
import org.junit.Test;

public class VersionScopedKeysTest {
private static final List<String> STORED = Arrays.asList(
"uuid",
"nativeConfig",
"bundleHashCache",
"packageVersion",
"currentVersion",
"hash_abc",
"hash_def",
"firstLoadMarked",
"nativeCheckIncomplete",
"nativeCheckResp",
null
);

// The round breadcrumb survives both: it only decides whether the next
// launch resumes the round without the start delay.
private static final HashSet<String> VERSION_KEYS = new HashSet<>(Arrays.asList(
"hash_abc", "hash_def", "firstLoadMarked", "nativeCheckResp"));

@Test
public void resetDropsVersionKeysAndKeepsInstallKeys() {
assertEquals(VERSION_KEYS, new HashSet<>(VersionScopedKeys.clearedOnReset(STORED)));
}

@Test
public void dropsVersionKeysAndKeepsInstallKeys() {
List<String> doomed = VersionScopedKeys.select(Arrays.asList(
"uuid",
"nativeConfig",
"bundleHashCache",
"packageVersion",
"currentVersion",
"hash_abc",
"hash_def",
"firstLoadMarked",
"nativeCheckIncomplete",
"nativeCheckResp",
null
));
assertEquals(
new HashSet<>(Arrays.asList(
"hash_abc",
"hash_def",
"firstLoadMarked",
"nativeCheckIncomplete",
"nativeCheckResp")),
new HashSet<>(doomed));
public void upgradeDropsVersionKeysAndKeepsInstallKeys() {
assertEquals(VERSION_KEYS, new HashSet<>(VersionScopedKeys.clearedOnUpgrade(STORED)));
}

@Test
Expand Down
90 changes: 59 additions & 31 deletions harmony/pushy/src/main/ets/UpdateContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,34 @@ function nativeConfigsEquivalent(a: string | undefined, b: string | undefined):
return NativePatchCore.configsEquivalent(a, b);
}

// Per-version info key prefix and the first-load marker key.
const HASH_INFO_PREFIX = 'hash_';
const FIRST_LOAD_MARKED_KEY = 'firstLoadMarked';

interface ClearedKeys {
keys: string[];
prefixes: string[];
}

// <generated:lifecycle> (scripts/contract.ts; bun scripts/generate-contract.ts)
// What a reset to the packaged bundle and a binary upgrade delete besides
// the state fields (scripts/contract.ts storedItems; every other item survives
// both: the uuid, the native config, the round breadcrumb, the bundle hash
// cache).
const RESET_CLEARED: ClearedKeys = {
keys: [FIRST_LOAD_MARKED_KEY, KEY_RESP_CACHE],
prefixes: [HASH_INFO_PREFIX],
};
const UPGRADE_CLEARED: ClearedKeys = {
keys: [FIRST_LOAD_MARKED_KEY, KEY_RESP_CACHE],
prefixes: [HASH_INFO_PREFIX],
};
// </generated:lifecycle>

interface PersistOptions {
removeStaleHash?: boolean;
cleanUp?: boolean;
markFirstLoadMarker?: boolean;
clearFirstLoadMarker?: boolean;
}

export class UpdateContext {
Expand Down Expand Up @@ -141,7 +164,7 @@ export class UpdateContext {
` cv=${snap.currentVersion} lv=${snap.lastVersion}` +
` ft=${snap.firstTime} fto=${snap.firstTimeOk}` +
` rb=${snap.rolledBackVersion}` +
` flm=${this.readString('firstLoadMarked')}` +
` flm=${this.readString(FIRST_LOAD_MARKED_KEY)}` +
` uuidSet=${!!this.readString('uuid')}`,
);
}
Expand Down Expand Up @@ -372,10 +395,7 @@ export class UpdateContext {
this.preferences.deleteSync(`hash_${state.staleVersionToDelete}`);
}
if (options.markFirstLoadMarker) {
this.preferences.putSync('firstLoadMarked', 'true');
}
if (options.clearFirstLoadMarker) {
this.preferences.deleteSync('firstLoadMarked');
this.preferences.putSync(FIRST_LOAD_MARKED_KEY, 'true');
}
if (options.cleanUp) {
this.cleanUp();
Expand Down Expand Up @@ -499,6 +519,27 @@ export class UpdateContext {
});
}

// Deletes `cleared` from the store (in memory; the caller flushes). A
// failed read leaves harmless orphans: none of them affects the state.
private removeKeys(cleared: ClearedKeys, event: string): void {
for (const key of cleared.keys) {
this.preferences.deleteSync(key);
}
if (cleared.prefixes.length === 0) {
return;
}
try {
const all = this.preferences.getAllSync();
for (const key of Object.keys(all)) {
if (cleared.prefixes.some(prefix => key.startsWith(prefix))) {
this.preferences.deleteSync(key);
}
}
} catch (e) {
logger.error(TAG, `Failed to clear keys on ${event}: ${getErrorMessage(e)}`);
}
}

public syncStateWithBinaryVersion(
packageVersion: string,
buildTime: string,
Expand All @@ -522,10 +563,11 @@ export class UpdateContext {
);
UpdateContext.ignoreRollback = false;
this.cleanUp();
// 仅重置状态机字段(currentVersion / lastVersion / firstTime / firstTimeOk /
// rolledBackVersion)。不再 clearExisting,避免连带清除 uuid / firstLoadMarked /
// hash_* 等与 binary 版本无关的 KV —— 它们在多实例场景下本就脆弱,连带清除会
// 让 getConstants() 永远读到空,从而 isFirstTime=false、markSuccess 永不执行。
// 旧二进制的版本已不在状态里:删掉它们的元信息、首启标记与响应缓存,与
// 各端一致(scripts/contract.ts storedItems)。uuid、原生配置等属于安装本身的
// 键保留——不走 clearExisting(那会让 getConstants 读到空的 uuid)。升级后
// 没有运行中的热更版本,首启标记不会在本次启动里先写后删。
this.removeKeys(UPGRADE_CLEARED, 'binary upgrade');
this.writeState(nextState, {});
this.flushInBackground('sync binary version');
}
Expand Down Expand Up @@ -629,25 +671,11 @@ export class UpdateContext {
this.beginFlushBatch();
let flushed: Promise<void> = Promise.resolve();
try {
// 删除已下载版本的 hash_* 元信息(不走 clear():它是异步的,且会连带清掉
// uuid —— 见 syncStateWithBinaryVersion 的注释)。读取失败时残留的
// hash_* 只是无害孤儿数据,不影响 reset 语义。
try {
const all = this.preferences.getAllSync();
for (const key of Object.keys(all)) {
if (key.startsWith('hash_')) {
this.preferences.deleteSync(key);
}
}
} catch (e) {
logger.error(
TAG,
`Failed to clear hash info on reset: ${getErrorMessage(e)}`,
);
}
this.writeState(resetState, { clearFirstLoadMarker: true });
// 缓存里的响应仍在宣告本次 reset 刚删掉的版本,一并丢弃,避免 JS 侧复用。
this.preferences.deleteSync(KEY_RESP_CACHE);
// 删除版本元信息、首启标记与响应缓存(缓存仍在宣告本次 reset 刚删掉的
// 版本),与各端一致(scripts/contract.ts storedItems)。不走 clear():
// 它是异步的,且会连带清掉 uuid。
this.removeKeys(RESET_CLEARED, 'reset');
this.writeState(resetState, {});
} finally {
flushed = this.endFlushBatch('reset to packaged bundle');
}
Expand Down Expand Up @@ -843,10 +871,10 @@ export class UpdateContext {
}

public consumeFirstLoadMarker(): boolean {
const marked = this.readString('firstLoadMarked') === 'true';
const marked = this.readString(FIRST_LOAD_MARKED_KEY) === 'true';
this.trace(`consumeFirstLoadMarker:marked=${marked}`);
if (marked) {
this.preferences.deleteSync('firstLoadMarked');
this.preferences.deleteSync(FIRST_LOAD_MARKED_KEY);
this.flushInBackground('clear first load marker');
}
return marked;
Expand Down
5 changes: 5 additions & 0 deletions ios/RCTPushy/RCTPushy+Launch.mm
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ + (NSURL *)resolveLaunchBundleURL:(NSString **)launchRolledBackVersionOut
PushyToStdString(curBuildTime)
);
if (sync.changed) {
// The old binary's versions are gone from the state: drop
// their info, the first-load marker and the response cache,
// as every platform does (scripts/contract.ts storedItems).
PushyRemoveKeys(defaults, PushyKeysClearedOnUpgrade(),
PushyKeyPrefixesClearedOnUpgrade());
[defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked()];
state = sync.state;
PushyApplyStateToDefaults(defaults, state);
Expand Down
15 changes: 5 additions & 10 deletions ios/RCTPushy/RCTPushy.mm
Original file line number Diff line number Diff line change
Expand Up @@ -445,16 +445,11 @@ - (NSDictionary *)constantsToExport
state.build_time = PushyToStdString([RCTPushy buildTime]);
PushyApplyStateToDefaults(defaults, state);

for (NSString *key in [defaults dictionaryRepresentation].allKeys) {
if ([key hasPrefix:keyHashInfo]) {
[defaults removeObjectForKey:key];
}
}
[defaults removeObjectForKey:keyFirstLoadMarked];
[defaults removeObjectForKey:KeyPackageUpdatedMarked()];
// A cached response still advertises the version this reset just
// removed; dropping it stops the JS side from reusing that answer.
[defaults removeObjectForKey:keyNativeCheckCache];
// Version info, the first-load and package-updated markers and the
// response cache (it still advertises the versions this reset just
// removed), the same on every platform (scripts/contract.ts).
PushyRemoveKeys(defaults, PushyKeysClearedOnReset(),
PushyKeyPrefixesClearedOnReset());
ignoreRollback = false;
});
} @catch (...) {
Expand Down
9 changes: 9 additions & 0 deletions ios/RCTPushy/RCTPushyInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ extern PUSHY_HIDDEN std::atomic<bool> ignoreRollback;
extern PUSHY_HIDDEN std::atomic<uint64_t> pushyResetGeneration;

PUSHY_HIDDEN NSString *KeyPackageUpdatedMarked(void);
// What a reset / a binary upgrade deletes besides the state fields
// (generated from scripts/contract.ts storedItems), and the removal itself:
// every key in `keys` and every stored key starting with one of `prefixes`.
PUSHY_HIDDEN NSArray<NSString *> *PushyKeysClearedOnReset(void);
PUSHY_HIDDEN NSArray<NSString *> *PushyKeyPrefixesClearedOnReset(void);
PUSHY_HIDDEN NSArray<NSString *> *PushyKeysClearedOnUpgrade(void);
PUSHY_HIDDEN NSArray<NSString *> *PushyKeyPrefixesClearedOnUpgrade(void);
PUSHY_HIDDEN void PushyRemoveKeys(NSUserDefaults *defaults, NSArray<NSString *> *keys,
NSArray<NSString *> *prefixes);
PUSHY_HIDDEN NSString *PushyHashInfoKey(NSString *hash);
// Immutable host-facing snapshot; activated always means NEXT launch.
PUSHY_HIDDEN NSDictionary *PushyHostResult(NSString *status, NSString *reason,
Expand Down
37 changes: 37 additions & 0 deletions ios/RCTPushy/RCTPushyState.mm
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,43 @@
// to reuse (§10.3), scoped to the request and config that produced it.
PUSHY_HIDDEN NSString *const keyNativeCheckCache = @"REACTNATIVECN_PUSHY_NATIVE_CHECK_RESP_KEY";

// <generated:lifecycle> (scripts/contract.ts; bun scripts/generate-contract.ts)
// What a reset to the packaged bundle and a binary upgrade delete besides
// the state fields (scripts/contract.ts storedItems; every other item survives
// both: the uuid, the native config, the round breadcrumb, the bundle hash
// cache).
PUSHY_HIDDEN NSArray<NSString *> *PushyKeysClearedOnReset(void) {
return @[keyFirstLoadMarked, KeyPackageUpdatedMarked(), keyNativeCheckCache];
}
PUSHY_HIDDEN NSArray<NSString *> *PushyKeyPrefixesClearedOnReset(void) {
return @[keyHashInfo];
}
PUSHY_HIDDEN NSArray<NSString *> *PushyKeysClearedOnUpgrade(void) {
return @[keyFirstLoadMarked, keyNativeCheckCache];
}
PUSHY_HIDDEN NSArray<NSString *> *PushyKeyPrefixesClearedOnUpgrade(void) {
return @[keyHashInfo];
}
// </generated:lifecycle>

PUSHY_HIDDEN void PushyRemoveKeys(NSUserDefaults *defaults, NSArray<NSString *> *keys,
NSArray<NSString *> *prefixes) {
for (NSString *key in keys) {
[defaults removeObjectForKey:key];
}
if (prefixes.count == 0) {
return;
}
for (NSString *key in [defaults dictionaryRepresentation].allKeys) {
for (NSString *prefix in prefixes) {
if ([key hasPrefix:prefix]) {
[defaults removeObjectForKey:key];
break;
}
}
}
}

// error def — messages are human-readable; the stable cross-platform codes
// live in cpp/patch_core/error_codes.h and travel in RCTPushyErrorCodeKey.
PUSHY_HIDDEN NSString *const ERROR_OPTIONS = @"options error";
Expand Down
Loading
Loading