feat(state): 迁移 itemNotifications(复用 ItemNotificationData)

This commit is contained in:
TermiNexus committed 2026-10-24 22:14:50 +08:00
1 parent bb544643b5
commit 6f68afa678
8 files changed
+251 -21

No files matched your search

+9 -5
View File
@@ -143,7 +143,7 @@
`names/root_wiring.json` + `TankiOffline/.../App/State/RootState.kt`、`RootWiring.kt`。
- 根状态 `z6`(`TOState`)的 **89 个槽位**全部入表;字段顺序取 `toString`,槽位取构造函数 `<名>_1`。
- 根 reducer `k6` 的逐切片委派记全:reducer 引用、委派表达式、顶层 `instanceof` 动作名。
- 已迁移切片 **86/89**:
- 已迁移切片 **87/89**:
- `inviteState` → `aD` / `pD.er5`(`InviteReducer.kt`)
- `loginByPassword` → `gD` / `MD.cr6`(`LoginReducer.kt`,含 `SetLogin`/`SetPassword` 把 status 打回 `NONE` 的原语义)
- `twitchState` → `g9` / `y9.gta`(`TwitchReducer.kt`;`SetAccount` 是 Twitch 账号,不是 UID)
@@ -435,12 +435,16 @@
`IgnoreUserMessages`/`ShowUserMessages` 增删屏蔽名单(`_C` 重载时清空);
`rN`(用户资料刷新)依赖消息内的用户对象,离线重建按 no-op;`SendMessageToServer` 的时间戳留给中间件。
消息复用生成物 `ChatMessage`。
- `itemNotifications` → `_1` / `A1().dso`(`ItemNotificationsReducer.kt`):1 字段,6 个分支。
`AddItemNotification(s)` 加入(构造 `ItemNotificationData`);`RemoveItemNotification` 先按有效期过滤,
再删掉匹配谓词的通知;`UpdateItemNotification` 对匹配项应用 handler;
`S1` 只做持久化(no-op);`_C` 重载时只保留有效期内的通知。
- `launchParams` → `(pass-through)`:原根 reducer 对 `hpx_1` 没有调用,缺省是 `ag()` 空 Map;
`RootState.LaunchParams` 收口为 `LaunchParamsStateSlice = Map<String, String>`,不新增 reducer/动作。
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 85 个有 reducer 的切片,
`launchParams` 与其余 **3 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 86/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **953 项全绿**,含 89 槽位完整性、85 个归约切片一次归约互不串片、未迁移字段透传,
- `App/State/RootReducer.kt`:`ReduceRoot(State0, Action)` 归约上述 86 个有 reducer 的切片,
`launchParams` 与其余 **2 个字段原样透传**;
迁移进度用 `RootReducer.Coverage`(`已迁移 87/89`)与 `RootWiring.Unmigrated` 暴露。
- 自检:`TankiOffline/tools/RunStateTest.ps1` **964 项全绿**,含 89 槽位完整性、86 个归约切片一次归约互不串片、未迁移字段透传,
以及用 `action_trace_merged.json` 真实派发的两条商店动作(`SetCoinPaymentProviderUrl` / `ToggleShopActivity`)回放。
- 委派表里的已知形态:`shop` / `home` / `localBattleUserState` / `partner` 是内联 IIFE,
已按字段具名为 `(inline@<field>)` 并从函数体抓到顶层动作名(69 / 1 / 2 / 11 个);
+11 -4
View File
@@ -1,5 +1,5 @@
{
"generatedAt": "2026-10-24T14:08:05.684Z",
"generatedAt": "2026-10-24T14:12:51.294Z",
"module": "module_10032.js",
"RootState": {
"Class": "z6",
@@ -92,7 +92,8 @@
"challenges",
"clansStorage",
"clan",
"chatState"
"chatState",
"itemNotifications"
],
"AnonymousActions": [
{
@@ -1636,6 +1637,12 @@
"Name": "SetChatConfig",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)"
},
{
"Local": "S1",
"Name": "PersistItemNotifications",
"DispatchedAt": "未在 action_trace_merged.json 中出现;按 reducer 分支效果定名",
"Effect": "通知列表变化后写回存储(纯 reducer 里 no-op)"
}
],
"Markers": [
@@ -3218,15 +3225,15 @@
"Reducer": "A1().dso",
"Delegate": "A1().dso(t, n.xpx_1)",
"Actions": [
"?S1",
"AddItemNotification",
"AddItemNotifications",
"PersistItemNotifications",
"RemoveItemNotification",
"ResetSettingsFromStorage",
"UpdateItemNotification"
],
"Default": "new _1()",
"Migrated": false
"Migrated": true
},
{
"Field": "contextTutorial",
+4
View File
@@ -368,6 +368,8 @@ const AnonymousActionNames = new Map([
['sN', 'MarkFirstChannelMessagesLoaded'],
// YP 的 toString 名 Configure 与匹配大厅同名类冲突,改名避免字段错配。
['YP', 'SetChatConfig'],
// itemNotifications 切片(_1 / I1.dso):S1 没有 toString。
['S1', 'PersistItemNotifications'],
]);
// 非动作的 `instanceof`:屏幕标识类,用于 GoTo 分支判断(不是 dispatch 的动作)。
@@ -858,6 +860,7 @@ const Migrated = {
clansStorage: { Type: 'ClanStorageStateSlice', Reduce: 'ReduceClanStorage' },
clan: { Type: 'ClanStateSlice', Reduce: 'ReduceClan' },
chatState: { Type: 'ChatStateSlice', Reduce: 'ReduceChat' },
itemNotifications: { Type: 'ItemNotificationsStateSlice', Reduce: 'ReduceItemNotifications' },
};
for (const R of Rows) R.Migrated = Object.prototype.hasOwnProperty.call(Migrated, R.Field);
@@ -1306,6 +1309,7 @@ const AppSettingsAnonymousEffects = {
qN: 'isNeedScroll=true',
sN: 'isFirstChannelMessagesLoad=false',
YP: 'config(原名 Configure,与匹配大厅同名类冲突,改名 SetChatConfig)',
S1: '通知列表变化后写回存储(纯 reducer 里 no-op)',
};
fs.writeFileSync(
path.join(P, 'names', 'root_wiring.json'),
@@ -0,0 +1,126 @@
// ItemNotificationsReducer.kt —— 物品「新」标记切片(对应 module_10032 的 _1 状态 + I1.dso)
//
// 证据链:
// 状态类 _1:1 个字段,toString 名 "ItemNotificationsState(notifications=…)"。
// 默认:notifications = emptySet()。
// 根委派:`A1().dso(t, n.xpx_1)`。
// reducer:
// q1(AddItemNotifications) → 批量加入(逐条去重)
// y1(AddItemNotification) → 用 6 个字段构造 `ItemNotificationData` 后加入
// $1(RemoveItemNotification) → 先按有效期过滤,再删掉匹配谓词的通知
// (section/itemId/type/parentId/excludeType;ssn_1 = 跳过有效期检查)
// j1(UpdateItemNotification) → 保留不匹配项,对匹配项应用 updateHandler
// S1(无 toString) → 只做持久化标记(纯 reducer 里 no-op)
// _C(ResetSettingsFromStorage)→ 从存储重载并过滤过期(Kotlin 侧按「只保留有效期内的」收口)
//
// 有效期判定 `msm`:`(startTime == null || startTime <= now) && (endTime == null || endTime > now)`。
// 通知模型直接复用生成物 `ItemNotificationData`(section / itemId / type / parentId / startTime / endTime)。
package App.State.Reducers
import App.State.AddItemNotification
import App.State.AddItemNotifications
import App.State.ItemNotificationData
import App.State.RemoveItemNotification
import App.State.UpdateItemNotification
/* ------------------------------------------------------------------ 匿名动作 */
/** 原 `S1`:通知列表变化后写回存储(纯 reducer 里 no-op)。 */
object PersistItemNotifications
/* ------------------------------------------------------------------ 状态 */
data class ItemNotificationsStateSlice(
val Notifications: Set<Any?> = emptySet(),
)
/* ------------------------------------------------------------------ reducer */
private fun Now(): Double = (js("Date.now()") as Number).toDouble()
private fun TimeOf(Value: Any?): Double? = (Value as? Number)?.toDouble()
/** 原 `msm`:有效期判定。 */
private fun InPeriod(Item: ItemNotificationData): Boolean {
val Now = Now()
val Start = TimeOf(Item.startTime)
val End = TimeOf(Item.endTime)
return (Start == null || Start <= Now) && (End == null || End > Now)
}
/** 原 `tsm`:删除 / 更新谓词(忽略最后一个 `skipPeriodCheck` 参数)。 */
private fun Matches(
Item: ItemNotificationData,
Section: Any?,
ItemId: Any?,
Type: Any?,
ParentId: Any?,
ExcludeType: Any?,
): Boolean {
val WantedId = (ItemId as? Number)?.toLong() ?: ItemId
return (Section == null || Item.section == Section) &&
(ItemId == null || Item.itemId == WantedId) &&
(Type == null || Item.type == Type) &&
(ParentId == null || Item.parentId == ParentId) &&
(ExcludeType == null || Item.type != ExcludeType)
}
private fun AsItem(Value: Any?): ItemNotificationData? = Value as? ItemNotificationData
fun ReduceItemNotifications(
State0: ItemNotificationsStateSlice,
Action: Any,
): ItemNotificationsStateSlice = when (Action) {
is AddItemNotifications ->
State0.copy(Notifications = State0.Notifications + Action.data)
is AddItemNotification -> State0.copy(
Notifications = State0.Notifications + ItemNotificationData(
section = Action.section,
itemId = Action.itemId,
type = Action.type,
parentId = Action.parentId,
startTime = Action.startTime,
endTime = Action.endTime,
),
)
is RemoveItemNotification -> State0.copy(
Notifications = State0.Notifications.filterNot {
val Item = AsItem(it) ?: return@filterNot false
val Skipped = Action.skipPeriodCheck == true
(Skipped || InPeriod(Item)) &&
Matches(
Item,
Action.section,
Action.itemId,
Action.type,
Action.parentId,
Action.excludeType,
)
}.toSet(),
)
is UpdateItemNotification -> State0.copy(
Notifications = State0.Notifications.map {
val Item = AsItem(it) ?: return@map it
if (!Matches(
Item,
Action.section,
Action.itemId,
Action.type,
Action.parentId,
Action.excludeType,
)
) {
it
} else {
@Suppress("UNCHECKED_CAST")
val Transform = Action.updateHandler as? Function1<ItemNotificationData, ItemNotificationData>
Transform?.invoke(Item) ?: it
}
}.toSet(),
)
is PersistItemNotifications -> State0
is ResetSettingsFromStorage -> State0.copy(
Notifications = State0.Notifications.filter { AsItem(it)?.let(::InPeriod) ?: true }.toSet(),
)
else -> State0
}
@@ -1,7 +1,7 @@
// RootReducer.kt —— 根 reducer 骨架(对应 module_10032 的根状态 z6 / TOState 与根 reducer k6)
//
// 原包 k6 对 89 个切片逐个调用各自的 reducer,再 `return n.cti(...)`(copy 出新状态)。
// 这里采用同一形状。已迁移的 86 个切片里,85 个真正调用 reducer,launchParams 只做类型收口:
// 这里采用同一形状。已迁移的 87 个切片里,86 个真正调用 reducer,launchParams 只做类型收口:
//
// inviteState → ReduceInvite(aD / pD.er5)
// loginByPassword → ReduceLoginByPassword(gD / MD.cr6)
@@ -88,9 +88,10 @@
// clansStorage → ReduceClanStorage(lO / me.vr1,根委派 `me.vr1(t, n.rpv_1)`)
// clan → ReduceClan(jN / we.hr0,根委派 `we.hr0(t, n.qpv_1)`)
// chatState → ReduceChat(LP / $r.wqv,根委派 `$r.wqv(t, n.opv_1)`)
// itemNotifications → ReduceItemNotifications(_1 / A1().dso,根委派 `A1().dso(t, n.xpx_1)`)
// launchParams → 无 reducer(原根 reducer 原样透传;默认 `ag()` = emptyMap())
//
// 其余 3 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 其余 2 个字段按原样透传(defaultValue 取原构造)。每个切片 reducer 对无关动作都返回原状态,
// 所以一次 copy 里串起全部已迁移 reducer,与按动作路由再归约的结果一致。
// 迁移进度与每个字段的证据见生成物 RootWiring.kt(不要手改)。
package App.State
@@ -132,6 +133,7 @@ import App.State.Reducers.ReduceGraphicsSettings
import App.State.Reducers.ReduceHome
import App.State.Reducers.ReduceIcons
import App.State.Reducers.ReduceInvite
import App.State.Reducers.ReduceItemNotifications
import App.State.Reducers.ReduceKeyboardSettings
import App.State.Reducers.ReduceLanguage
import App.State.Reducers.ReduceLocalBattleUser
@@ -279,6 +281,7 @@ object RootReducer {
ClansStorage = ReduceClanStorage(State0.ClansStorage, Action),
Clan = ReduceClan(State0.Clan, Action),
ChatState = ReduceChat(State0.ChatState, Action),
ItemNotifications = ReduceItemNotifications(State0.ItemNotifications, Action),
)
}
@@ -42,6 +42,7 @@ import App.State.Reducers.GraphicsSettingsStateSlice
import App.State.Reducers.HomeStateSlice
import App.State.Reducers.IconsStateSlice
import App.State.Reducers.InviteStateSlice
import App.State.Reducers.ItemNotificationsStateSlice
import App.State.Reducers.KeyboardSettingsStateSlice
import App.State.Reducers.LobbyLoaderStateSlice
import App.State.Reducers.LocalBattleUserStateSlice
@@ -248,8 +249,8 @@ data class RootState(
val TwitchState: TwitchStateSlice = TwitchStateSlice(),
/** 原 icons(oH / vs.kru)。已迁移。 */
val Icons: IconsStateSlice = IconsStateSlice(),
/** 原 itemNotifications(_1 / A1().dso)。未迁移:原样透传。 */
val ItemNotifications: Any? = null,
/** 原 itemNotifications(_1 / A1().dso)。已迁移。 */
val ItemNotifications: ItemNotificationsStateSlice = ItemNotificationsStateSlice(),
/** 原 contextTutorial(Mst / Jst().tth)。已迁移。 */
val ContextTutorial: ContextTutorialStateSlice = ContextTutorialStateSlice(),
/** 原 suppliesTutorial(ust / Uw.uth)。已迁移。 */
@@ -91,7 +91,7 @@ object RootWiring {
SliceWiring("rewardedVideosState", "upx_1", "JQ", "xh.zsl", "xh.zsl(t, n.upx_1)", listOf("SetDisplayState")),
SliceWiring("twitchState", "vpx_1", "g9", "uf.gta", "uf.gta(t, n.vpx_1)", listOf("SetAccount", "SetDaysForNextLinking", "SetTwitchEnabled")),
SliceWiring("icons", "wpx_1", "oH", "vs.kru", "vs.kru(t, n.wpx_1)", listOf("SetIcons")),
SliceWiring("itemNotifications", "xpx_1", "_1", "A1().dso", "A1().dso(t, n.xpx_1)", listOf("?S1", "AddItemNotification", "AddItemNotifications", "RemoveItemNotification", "ResetSettingsFromStorage", "UpdateItemNotification")),
SliceWiring("itemNotifications", "xpx_1", "_1", "A1().dso", "A1().dso(t, n.xpx_1)", listOf("AddItemNotification", "AddItemNotifications", "PersistItemNotifications", "RemoveItemNotification", "ResetSettingsFromStorage", "UpdateItemNotification")),
SliceWiring("contextTutorial", "ypx_1", "Mst", "Jst().tth", "Jst().tth(t, n.ypx_1)", listOf("LoadCurrentElement", "MarkElementAsViewed", "SetLoadedElements", "SetMissionToElement", "SetUnViewedElements")),
SliceWiring("suppliesTutorial", "zpx_1", "ust", "Uw.uth", "Uw.uth(t, n.zpx_1)", listOf("ActivatedInCurrentBattle", "SetLoadedElement")),
SliceWiring("questCategories", "apy_1", "zZ", "th.lsh", "th.lsh(t, n.apy_1)", listOf("RemoveQuestCategory", "RequestQuestCategoriesStatistic", "SetQuestCategories", "SetQuestCategoriesStatistic")),
@@ -106,7 +106,7 @@ object RootWiring {
)
/** 已迁移(强类型;launchParams 为透传,不调用 reducer)的字段。 */
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState")
val Migrated: Set<String> = setOf("inviteState", "loginByPassword", "twitchState", "home", "localBattleUserState", "shop", "screen", "lootBoxes", "garage", "garagePreview", "entrance", "user", "settings", "accountSettings", "proBattle", "language", "announcement", "launchParams", "news", "notifications", "quests", "lobbyLoader", "purchaseDialogState", "promoCode", "supportUrl", "rulesUpdated", "notificationByCountry", "availabilityByRank", "icons", "questCategories", "mainQuest", "securitySettings", "referralEvent", "tipsState", "rankUpDialogState", "battleResult", "captchaState", "modalScreen", "droppableGold", "eventGoods", "creatorTagState", "rewardedVideosState", "passwordChangeState", "system", "contextTutorial", "battleTutorial", "spectator", "twoStepVerificationState", "antiAddiction", "moderation", "migrationState", "register", "userReportsState", "friends", "suppliesState", "clanUserInfo", "battleInviteState", "partner", "userRenameState", "battlePauseState", "battleViewState", "tankActionsLogState", "suppliesTutorial", "clanChatState", "battleSearchState", "battleStatistics", "soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings", "graphicsSettings", "controlSettings", "battleSettings", "contractState", "tankBuilderState", "battleMessage", "maps", "battleChat", "remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications")
/** 未迁移字段:归约时原样透传,作为后续迁移清单。 */
val Unmigrated: List<SliceWiring> get() = All.filter { it.Field !in Migrated }
@@ -19,7 +19,7 @@
// System / ContextTutorial / BattleTutorial / Spectator / TwoStepVerification / AntiAddiction /
// Moderation / MigrationState / Register / UserReports /
// Friends / Supplies / ClanUserInfo / BattleInvite / Partner / UserRename /
// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges / ClansStorage / Clan / ChatState;
// BattlePause / BattleView / TankActionsLog / SuppliesTutorial / ClanChat / BattleSearch / BattleStatistics / SoundSettings / GameSettings / KeyboardSettings / TouchscreenHudSettings / GraphicsSettings / ControlSettings / BattleSettings / ContractState / TankBuilder / BattleMessage / Maps / BattleChat / RemoteUserDataStorage / Matchmaking / BattlesSelect / BattleUsers / Challenges / ClansStorage / Clan / ChatState / ItemNotifications;
// ㉑ launchParams 是纯透传切片(无 reducer/动作),只做 Map 类型收口;⑭ 根 reducer 的 89 槽位委派
//
// 用法:$env:TANKIOFFLINE_STATE_TEST='1'; node …(见 TankiOffline/tools/RunStateTest.ps1)
@@ -904,6 +904,14 @@ import App.State.Reducers.RequireHideChatKeyboard
import App.State.Reducers.ResetChat
import App.State.Reducers.SetChatConfig
import App.State.Reducers.ShowChatKeyboard
import App.State.AddItemNotification
import App.State.AddItemNotifications
import App.State.ItemNotificationData
import App.State.RemoveItemNotification
import App.State.UpdateItemNotification
import App.State.Reducers.ItemNotificationsStateSlice
import App.State.Reducers.PersistItemNotifications
import App.State.Reducers.ReduceItemNotifications
import App.State.Reducers.UserRankBounds
import App.State.Reducers.UserStateSlice
import App.State.Reducers.ClearHullData
@@ -1331,7 +1339,7 @@ object StateCheck {
Check("切片 LocalBattleUser:接 Store 派发后状态已归约", Store.State.IsReArmorTemporaryDisabled)
}
// ⑭ 根 reducer:89 个槽位的委派表 + 86 个已迁移切片(85 归约 + launchParams 透传)
// ⑭ 根 reducer:89 个槽位的委派表 + 87 个已迁移切片(86 归约 + launchParams 透传)
run {
Check("根:根状态 89 个切片槽位", RootWiring.All.size == 89)
Check(
@@ -1395,7 +1403,7 @@ object StateCheck {
LocalBattleW.Reducer == "(inline@localBattleUserState)"
)
Check(
"根:迁移表 = 86 个已迁移字段,其余 3 个待迁",
"根:迁移表 = 87 个已迁移字段,其余 2 个待迁",
RootReducer.Migrated ==
setOf(
"inviteState", "loginByPassword", "twitchState", "home",
@@ -1419,9 +1427,9 @@ object StateCheck {
"soundSettings", "gameSettings", "keyboardSettings", "touchscreenHudSettings",
"graphicsSettings", "controlSettings", "battleSettings", "contractState",
"tankBuilderState", "battleMessage", "maps", "battleChat",
"remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState",
"remoteUserDataStorage", "matchmaking", "battlesSelect", "battleUsers", "challenges", "clansStorage", "clan", "chatState", "itemNotifications",
) &&
RootWiring.Unmigrated.size == 3
RootWiring.Unmigrated.size == 2
)
// 一次 ReduceRoot 里多个切片各归约自己的动作,互不串片
@@ -1477,7 +1485,7 @@ object StateCheck {
S5.User.Uid == "user-1" && S5.Screen.Current == NamedScreen("Preloader")
)
Check("根:无关动作返回等值状态", ReduceRoot(S5, AddFrags(1)) == S5)
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 86/89")
Check("根:Coverage 文本", RootReducer.Coverage == "已迁移 87/89")
val Store = ReduxStore(RootState(), { S, A -> ReduceRoot(S, A) })
Store.Dispatch(SetStatus("LOGGING_IN"))
@@ -7346,6 +7354,83 @@ object StateCheck {
)
}
// 97 切片 reducer:ItemNotifications(_1 / A1().dso,1 字段;6 个动作)
run {
fun Item(Id: Long, End: Any? = null): ItemNotificationData = ItemNotificationData(
section = "shop", itemId = Id, type = "new", parentId = null,
startTime = null, endTime = End,
)
val S0 = ItemNotificationsStateSlice()
Check("切片 ItemNotifications:默认空集合", S0.Notifications.isEmpty())
val S1 = ReduceItemNotifications(S0, AddItemNotification("shop", 1L, "new", null, null, null))
Check(
"切片 ItemNotifications:AddItemNotification 构造并入集合",
S1.Notifications.size == 1 &&
(S1.Notifications.first() as ItemNotificationData).itemId == 1L
)
val S2 = ReduceItemNotifications(S1, AddItemNotifications(listOf(Item(2L), Item(3L))))
Check(
"切片 ItemNotifications:AddItemNotifications 批量加入",
(S2.Notifications.map { (it as ItemNotificationData).itemId }).toSet() ==
setOf(1L, 2L, 3L)
)
Check(
"切片 ItemNotifications:RemoveItemNotification 按 section+itemId 删",
ReduceItemNotifications(
S2,
RemoveItemNotification("shop", 2L, null, null, null, true),
).Notifications.size == 2
)
Check(
"切片 ItemNotifications:excludeType 命中时保留",
ReduceItemNotifications(
S2,
RemoveItemNotification("shop", null, null, null, "new", true),
).Notifications.size == 3
)
Check(
"切片 ItemNotifications:UpdateItemNotification 对匹配项应用 handler",
ReduceItemNotifications(
S2,
UpdateItemNotification("shop", 1L, null, null, null, { Item: ItemNotificationData ->
Item.copy(section = "updated")
}),
).let { Updated ->
(Updated.Notifications.first { (it as ItemNotificationData).itemId == 1L }
as ItemNotificationData).section == "updated"
}
)
Check(
"切片 ItemNotifications:PersistItemNotifications 是 no-op",
ReduceItemNotifications(S2, PersistItemNotifications) == S2
)
Check(
"切片 ItemNotifications:过期项在重载时被过滤",
ReduceItemNotifications(
ReduceItemNotifications(S0, AddItemNotification("shop", 9L, "new", null, null, 1L)),
ResetSettingsFromStorage,
).Notifications.isEmpty()
)
Check(
"切片 ItemNotifications:无关动作不改状态",
ReduceItemNotifications(S2, AddFrags(1)) == S2
)
val Root0 = RootState()
val Root1 = ReduceRoot(Root0, AddItemNotification("shop", 1L, "new", null, null, null))
Check(
"根:AddItemNotification 只动 itemNotifications",
Root1.ItemNotifications.Notifications.size == 1 &&
Root1.ChatState == Root0.ChatState
)
val W = RootWiring.ByField("itemNotifications")!!
Check(
"根:itemNotifications 委派到 _1 / A1().dso 且 6 个动作",
W.SliceClass == "_1" && W.Reducer == "A1().dso" && W.Actions.size == 6
)
}
val Failed = Results.filter { !it.second }
println("TankiOffline 状态层自检:${Results.size} 项")
for ((Name, Ok) in Results) println((if (Ok) " ✓ " else " ✗ ") + Name)