data-layer-footguns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Data-layer footguns: Room, SQL and repositories

数据层陷阱:Room、SQL 与 repository

26 traps mined from a production Kotlin and Compose Multiplatform app, one short file each under
references/
. Every file gives the working pattern, the Traps (the specific ways it fails in practice, and why), and a Verifying it section with commands to run against the user's own tree.
How to use this index. Match the code about to be written, or the symptom being chased, against the entries below: each names its topic and the symptom that should send you to it. Read every file that plausibly applies with the Read tool before proposing code or a fix; each is under 150 lines. Where a Verifying command names the source project's paths, substitute the equivalent paths in the user's tree.
Cross-references. A backticked trap name inside a file that is not listed here lives in a sibling area skill: glob
../*/references/<name>.md
to open it.
从生产级 Kotlin 与 Compose Multiplatform 应用中提炼出的 26 个陷阱,每个陷阱在
references/
下对应一个简短文件。每个文件都会给出可行模式、陷阱(实际失败的具体方式及原因),以及 验证方法 部分,其中包含可针对用户自己的代码库运行的命令。
如何使用此索引。 将准备编写的代码或正在排查的症状与下面的条目进行匹配:每个条目都标明了主题,以及应据此查阅该条目的症状。在提出代码或修复方案之前,使用 Read 工具阅读所有可能适用的文件;每个文件都不到 150 行。如果验证命令中提到了源项目的路径,请替换为用户代码库中的等效路径。
交叉引用。 如果文件内使用了反引号包裹的陷阱名称,但该名称未在此列出,那么它位于同级领域的 skill 中:使用 glob
../*/references/<name>.md
打开它。

Databases and SQL

数据库与 SQL

  • cascading-delete-ordering
    — Sweeping a local cache database down to what the user actually owns — ordering container deletes before leaf deletes, telling "kept by state" columns apart from genuine garbage, re-checking conditions inside the DELETE, and pinning the record currently in use. Reach for it when a "clear cache" or "clear history" pass completes without error and frees nothing, when it instead wipes the user's favourites or downloads, or when the item playing on screen vanishes mid-sweep.
  • sql-not-in-nullable-trap
    — Why
    x NOT IN (subquery)
    matches zero rows and still reports success whenever a NULL is actually present in the subquery result — a standing risk for any nullable column — how to guard every such subquery, and how to make a silently-inert statement detectable instead of invisible. Reach for it when a DELETE or SELECT with a NOT IN filter returns nothing on data you can see with your own eyes, when a cleanup pass "succeeds" and frees nothing, or before writing any NOT IN over a nullable column.
  • like-wildcard-escaping-ids
    — Matching a machine-generated id inside a text or JSON column with LIKE — why
    _
    and
    %
    in the id silently widen the match, how to escape them with nested replace() plus an explicit ESCAPE character, and why the id must be matched as a quoted token rather than as a bare substring. Reach for it when a cleanup spares rows it should have deleted, when a lookup returns a row belonging to a different id, or before putting any id into a LIKE pattern.
  • room-rawquery-readonly-vacuum
    — Why a raw-query DAO method runs on a read-only connection, so database compaction fails there with "attempt to write a readonly database" while a checkpoint on the same connection succeeds and hides the problem. Reach for it when a bulk delete frees no disk space, or when a raw statement reports a readonly database you clearly opened for writing.
  • clean-satellite-at-transition
    — Deleting the dependent rows that exist only to serve a state at the exact transition that ends that state — rather than leaving them to a later bulk sweep — and why a foreign key cannot do this for you when only a flag changes. Reach for it when tables grow without bound and nothing ever deletes from them, when rows survive whose parent no longer justifies them, or when a bulk cleanup has to reason about rows whose parent has already been swept away.
  • room-kmp-setup
    — Set up one Room database shared across Android, JVM/desktop and iOS with an expect/actual builder per platform, a bundled SQLite driver chosen once at the injection site, and a per-architecture audit of the driver artifact. Use when adding Room to a Kotlin Multiplatform module, when one target fails at the first database connection while the others work, or when a target compiles but its generated database implementation is missing.
  • room-migrations-at-scale
    — Keep a Room database upgradable after twenty-plus schema versions — declaring the whole graph of (from,to) edges instead of assuming users only ever hop one version, filling the gaps a generated migration cannot express, and recreating triggers idempotently in the on-open callback. Use when an upgrade from an old build reports no migration path, when a trigger exists on upgraded databases but not on fresh installs (or the reverse), or before shipping a schema change to a long-lived app.
  • datastore-kmp-manager
    — Build a multiplatform preferences manager — the store instance produced per platform from nothing but a file path, one observing flow plus one suspend setter per key, and the interface declared in the domain layer so feature code never imports the storage library. Use when adding shared settings to a Kotlin Multiplatform app, when a setting reads back as its default after an upgrade, or when a settings screen shows a stale value until it is reopened.
  • local-listening-analytics
    — Build per-user listening or usage analytics entirely on-device — an append-only event table plus a denormalized per-contributor table that carries a copy of the timestamp, two completion thresholds instead of one, bare ids in events enriched to titles and artwork only at read time, and every window query parameterised as (start, end) so "last N days" stays an argument. Use when adding a "your year in review" or top-items screen without a backend, when a time-window chart is slow, when a chart is mysteriously shorter than the row count says it should be, or when a per-contributor total refuses to add up to the period's own figure.
  • import-format-contract-design
    — Specify a user-facing exchange file — why a version field can be worse than none, rejecting a file whose parse yields nothing, stating the same-length rule that positionally-aligned arrays imply, naming the legacy values a producer must never emit, and, when the producer is someone else's published format, placing values by the key that means something instead of by position. Use when designing an import/export or backup format, when writing a parser for a published one, or when a user reports importing a file and getting nothing with no error.
  • bulk-json-import-progress
    — Import thousands of rows from a user-supplied file without the UI going dark or the batch dying halfway — chunk the writes and emit progress per chunk, reject a parse that yields nothing before touching the database, and filter incoming rows down to those whose referenced parents exist. Use when building an import/restore feature, when an import of a large file appears frozen, or when a single bad row aborts a whole import.
  • stored-timestamp-is-a-local-wall-clock
    — A timestamp column written through an ORM type converter can hold the local wall clock encoded as if it were UTC — an exact round trip that is only correct through the converter, and the converter is chosen by the field's TARGET TYPE, so declaring a projection field as a raw number silently opts out and applies the offset a second time. Covers why every total still adds up, why the error is exactly zero on some machines, and why the fix is asking for the type the converter understands rather than picking a time zone. Use when an hour-of-day or day-of-week breakdown is shifted by your own offset while every count and sum is right, when a chart says people are most active at 3am, or before typing a stored time column as a number in a hand-written projection.
  • bucket-local-time-in-code-not-in-sql
    — Group events by local hour or local day in application code from one raw scan, not in SQL — the engine's local-time modifier answers from the process time zone rather than the user's, and four local-time aggregates mean four scans that can disagree with each other. Covers where the line sits between an aggregate that belongs in SQL and one that does not, and what the single scan has to return to stay correct. Use when an hour-of-day or weekday chart differs between platforms or between a device and a desktop build, when adding a fourth "group by day" query, or before writing a date function into a query string.
  • unbounded-for-shares-capped-for-lists
    — A top-N query is right for a list and wrong for a share-of-the-whole — the cut tail shrinks the denominator and the entropy normaliser, inflating concentration and diversity alike — so the same grouped data needs two queries with different bounds. Covers why the truncation is invisible in the result, why the unbounded query's ORDER BY becomes load-bearing, and when unbounded is actually safe. Use when a "top 5 share" or diversity score reads implausibly high, when one grouped query is feeding both a leaderboard and a statistic, or before reusing a capped DAO method for anything that divides by a total.
  • first-ever-not-first-in-window
    — Counting entities encountered for the first time means taking MIN over the entity's whole history and asking whether that minimum lands inside the window — the natural version, which filters to the window and then groups, calls every entity new. Covers why the wrong query passes its first test, why the window belongs in HAVING rather than WHERE, what an unbounded scan needs from the index, and how a retention prune quietly redefines "ever". Use when a "new this period" figure tracks the distinct count exactly, when discovery rate is implausibly close to 1, or before writing any first-seen query.
  • inclusive-period-boundaries-and-offset-reset
    — Pick one boundary convention for a stepping period navigator and hold it everywhere — a closed upper bound at 23:59:59 drops the last second's sub-second remainder, and half-open bounds handed to an inclusive BETWEEN double-count the shared instant — then reset the step offset whenever the granularity changes, because N periods back at one length is not N periods back at another. Covers clamping at the present, deriving the forward affordance from the same value, and why the current period's totals are not comparable to the previous one's. Use when a period navigator lands on the wrong span after switching granularity, when a boundary event is missing or counted twice, or when a first-of-the-month comparison reads catastrophically low.
  • cascading-delete-ordering
    — 将本地缓存数据库清理至用户实际拥有的内容——先删除 container 记录,再删除 leaf 记录;区分“由 state 保留”的列与真正的垃圾数据;在 DELETE 内部重新检查条件;并固定当前正在使用的记录。当“清除缓存”或“清除历史”流程执行完毕且没有报错,却没有释放任何空间;反而删除了用户的收藏或下载内容;或屏幕正在播放的项目在清理过程中消失时,查阅此项。
  • sql-not-in-nullable-trap
    — 为什么
    x NOT IN (subquery)
    在子查询结果中确实存在 NULL 时会匹配零行,却仍报告成功——任何可空列都长期存在这一风险——如何防范每一个此类子查询,以及如何让静默失效的语句能够被检测到,而不是悄无声息。当 DELETE 或 SELECT 带有 NOT IN 过滤器,对肉眼可见的数据却什么都没返回;清理流程“成功”却没有释放空间;或准备对可空列编写任何 NOT IN 时,查阅此项。
  • like-wildcard-escaping-ids
    — 使用 LIKE 在文本或 JSON 列中匹配机器生成的 id——为什么 id 中的
    _
    和
    %
    会在无感知的情况下扩大匹配范围,如何通过嵌套 replace() 加上显式 ESCAPE 字符进行转义,以及为什么必须将 id 作为带引号的 token 匹配,而不能只匹配裸子字符串。当清理过程漏掉本应删除的行、查询返回了属于其他 id 的行,或准备将任何 id 放入 LIKE 模式时,查阅此项。
  • room-rawquery-readonly-vacuum
    — 为什么 raw-query DAO 方法运行在只读连接上,因此数据库压缩会在该处因 "attempt to write a readonly database" 而失败,而同一连接上的 checkpoint 却能成功并掩盖问题。当批量删除没有释放磁盘空间,或 raw statement 报告 "readonly database",而你明明是以可写方式打开数据库时,查阅此项。
  • clean-satellite-at-transition
    — 删除仅用于服务某个 state、并恰好在该 state 结束转换时存在的依赖行,而不是将它们留给之后的批量清理;同时说明当只有 flag 发生变化时,外键为何无法代劳。当表不断增长却从未删除任何内容、父记录已无法证明这些行仍有存在依据却依然残留,或批量清理必须处理父记录已被清除的行时,查阅此项。
  • room-kmp-setup
    — 设置一个跨 Android、JVM/desktop 和 iOS 共享的 Room 数据库:为每个平台提供 expect/actual builder,在注入位置一次性选定随包的 SQLite driver,并按架构审计 driver artifact。向 Kotlin Multiplatform 模块添加 Room、某个目标在首次数据库连接时失败而其他目标正常,或某个目标能够编译却缺少生成的数据库实现时,使用此项。
  • room-migrations-at-scale
    — 让 Room 数据库在二十多个 schema 版本之后仍可升级——声明完整的 (from,to) 边图,而不是假设用户只会跨越一个版本;填补自动生成的 migration 无法表达的缺口;并在 on-open callback 中幂等地重新创建 trigger。当从旧版本升级时报告没有 migration 路径、升级后的数据库存在 trigger 而全新安装没有(或反之),或准备向长期运行的应用发布 schema 变更时,使用此项。
  • datastore-kmp-manager
    — 构建多平台 preferences manager——每个平台仅通过文件路径创建 store 实例,每个 key 配备一个用于观察的 flow 和一个 suspend setter,并在 domain layer 声明 interface,使 feature code 永不导入 storage library。向 Kotlin Multiplatform 应用添加共享设置、升级后设置读取结果却是默认值,或设置页在重新打开前一直显示过期值时,使用此项。
  • local-listening-analytics
    — 完全在设备端构建按用户划分的收听或使用分析——使用 append-only event table 和反规范化的 per-contributor table,后者携带 timestamp 副本、设置两个而非一个 completion threshold;事件中只保存裸 id,直到读取时才补充为 title 和 artwork;每个窗口查询参数都设为 (start, end),让“最近 N 天”始终作为参数。添加“年度回顾”或热门项目页面但不设后端、时间窗口图表运行缓慢、图表显示的长度莫名其妙地短于行数所暗示的长度,或某个 per-contributor 总数始终无法与该周期自身的总数吻合时,使用此项。
  • import-format-contract-design
    — 定义面向用户的交换文件——为什么 version field 可能比完全没有版本更糟糕、如何拒绝解析后没有任何内容的文件、如何明确 positional-aligned arrays 隐含的等长规则、指出 producer 绝不能发出的 legacy values,以及当 producer 使用他人发布的格式时,如何按有意义的 key 而非位置来放置值。设计 import/export 或 backup 格式、编写已发布格式的 parser,或用户报告导入文件后没有任何结果且没有错误时,使用此项。
  • bulk-json-import-progress
    — 从用户提供的文件导入数千行,同时避免 UI 卡死或批处理在半途失败——分块执行写入并按块发出进度;在触碰数据库前拒绝解析结果为空的输入;并将传入行过滤为仅保留所引用 parent 确实存在的行。构建 import/restore 功能、大文件导入看似冻结,或单条坏行导致整批导入中止时,使用此项。
  • stored-timestamp-is-a-local-wall-clock
    — 通过 ORM type converter 写入的 timestamp column 可能保存本地 wall clock,却被编码成仿佛是 UTC——一次精确往返,但只有通过 converter 才能正确实现;converter 由字段的 TARGET TYPE 决定,因此将 projection field 声明为原始数字会悄悄退出该转换,并第二次应用 offset。涵盖为何每个总数仍能相加、为何某些机器上的误差恰好为零,以及为何修复方法是请求 converter 能够理解的 type,而不是选择 time zone。当按 hour-of-day 或 day-of-week 分解的结果因自身 offset 而偏移,但每个 count 和 sum 都正确;图表显示用户在凌晨 3 点最活跃;或准备在手写 projection 中将 stored time column 用作数字时,查阅此项。
  • bucket-local-time-in-code-not-in-sql
    — 在应用代码中基于本地 hour 或 day 对事件进行分组,而不是在 SQL 中——引擎的 local-time modifier 会根据进程 time zone 而不是用户的时区作答;四次 local-time aggregate 意味着四次扫描,而且结果可能互不一致。说明 SQL aggregate 与应用代码 aggregate 的界线,以及单次扫描必须返回哪些内容才能保持正确。当按 hour-of-day 或 weekday 得到的图表在不同平台之间,或在设备与 desktop build 之间存在差异;准备添加第四个“按天分组”查询;或准备把 date function 写入 query string 时,查阅此项。
  • unbounded-for-shares-capped-for-lists
    — Top-N query 适用于 list,却不适用于 share-of-the-whole——截断尾部会缩小分母和 entropy normaliser,同时夸大 concentration 与 diversity——因此同一组 grouped data 需要两个 bounds 不同的 query。涵盖为何截断在结果中不可见、为何 unbounded query 的 ORDER BY 变得至关重要,以及 unbounded 何时实际安全。当“前 5 名占比”或 diversity score 高得不合常理、同一 grouped query 同时用于 leaderboard 和 statistic,或准备把有上限的 DAO method 复用于任何涉及除以总数的地方时,查阅此项。
  • first-ever-not-first-in-window
    — 统计首次遇到的实体,意味着对实体的完整历史取 MIN,再判断该最小值是否落在窗口内——自然版本先过滤到窗口再分组,会把每个实体都算作 new。涵盖错误 query 为何能通过首次测试、窗口为何应放在 HAVING 而不是 WHERE 中、unbounded scan 需要 index 提供什么,以及 retention prune 如何悄然重新定义“ever”。当“本期新增”数字与 distinct count 完全一致、discovery rate 高得离谱地接近 1,或准备编写任何 first-seen query 时,查阅此项。
  • inclusive-period-boundaries-and-offset-reset
    — 为步进式 period navigator 选定一种边界约定并始终统一执行——闭区间上界设为 23:59:59 会丢掉最后一秒的亚秒余量,而将半开边界传给 inclusive BETWEEN 会对共享时刻重复计数——并在 granularity 改变时重置 step offset,因为按一种长度回退 N 个周期,并不等于按另一种长度回退 N 个周期。涵盖在当前时刻截断、如何从同一值推导向前浏览能力,以及为何当前周期的 totals 不能与上一周期直接比较。当 period navigator 在切换 granularity 后落在错误区间、边界事件缺失或被重复计算,或 first-of-the-month 比较低得离谱时,查阅此项。

Repositories, caching and paging

Repository、缓存与分页

  • repository-flow-conventions
    — One table of method shapes for a repository sitting over a local database plus a remote API — local reads as a cold flow moved onto the IO dispatcher, remote reads as a flow of a success/error envelope, writes as withContext. Use when adding methods to a repository, when reviewing one whose shapes have drifted apart, or when a screen sits on its loading state forever with nothing in the log.
  • cache-then-network
    — Serve the stored copy immediately, then the fresh one, from a single repository flow — and emit an error only when nothing was served, because an error after a successful emission replaces working content the user is already reading. Use when a screen shows a spinner on every open despite having shown the same data a minute ago, or when a brief network failure blanks a screen that had perfectly good content on it.
  • ttl-keyed-json-cache-lenient-decode
    — A small keyed cache for values that drift — each entry carries the moment it was fetched and answers an isStale check against a time-to-live constant, the whole map is stored as one JSON string in key-value preferences, and decoding is lenient plus wrapped so a schema change degrades to a cache miss instead of destroying every entry. Use when caching resolved covers, lookups or per-key results without a database table, or when a cache stopped working entirely after a model field was added.
  • generic-paged-db-accumulator
    — One reusable helper that reads an entire table through a (limit, offset) data-access function in a bounded loop, stopping on the first short page — plus when reading everything is legitimate (export, backup, bulk mapping) and the smell that means you needed a real query instead. Use when several repositories each hand-roll the same paging loop, or when a "read all" call gets slower than linearly as the table grows.
  • continuation-token-pagination-contract
    — Model an endpoint that returns a page plus a next-token as Flow<Resource<Pair<items, token?>>> — a null token means the end, the caller stores only the token, and a bounded prefetch primes the first pages before anything is shown. Use when wiring an opaque-cursor API into a repository, or when a list stops loading after one failed request and never recovers, or when paging fires twice for one trigger.
  • encoded-continuation-tokens-local-sort
    — Carry locally sorted and shuffled paging through the same token slot a remote API uses, by prefixing the token with a mode tag and encoding a cursor after it — and reject an unrecognised prefix loudly, because a silently ignored token leaves the pager stuck in its in-flight state and the list never loads again. Use when one list must page from either a server cursor or a local ordering, or when local sorting made paging stop working with nothing in the log.
  • order-preserving-section-mapping
    — Map a sectioned API response as a list of (title, items) in the order it arrived, never by reading result[0] and result[1] into named fields — a signed-in account is recorded here as getting an extra section pushed in front, and any such shift mislabels every section after it and drops the last one with no error. Assume the set and order may also vary by locale or region, and capture two responses to find out. Use when modelling a home feed or browse screen made of shelves, or when a screen shows the right content under the wrong headings for some users only.
  • remote-index-cached-as-rows-with-validator
    — Cache a large published index by parsing it into indexed rows once and re-checking it with the server's own ETag, so a routine freshness check costs a couple of hundred bytes — replaying the stored validator only while rows exist, treating a 200 that parses to nothing as a failure, and never letting a failed check refresh the timestamp. Use when a browsable catalogue is fetched from a static host, or when a cached index went empty and never refilled.
  • search-over-a-paged-list-queries-the-source
    — Filtering a paging stream only ever searches the pages already loaded, so whether an item is findable depends on how far the user happened to scroll — the search must query the store and render as a sibling overlay, leaving the paged reader and its drag-reorder, in-place removal and scroll position untouched. Covers the debounce and minimum-length gate, why the escaping belongs one layer above the query, and the one case where filtering in memory is correct. Use when search misses items that are definitely there, when results change after scrolling, or before threading a second data source through a paged list.
  • mirror-local-state-to-a-remote-account
    — An opt-in switch that mirrors a local flag onto a signed-in remote account — turning it on back-fills everything already flagged, turning it off stops mirroring and deliberately does not undo, and the per-item call is three-valued so "not attempted" is distinguishable from "failed". Covers writing locally first and unconditionally, which of the two paths is allowed to speak to the user, and why the back-fill is sequential. Use when a mirrored flag silently disagrees with the account, when the user cannot tell a failure from a no-op, or before wiring a settings switch to a remote write.
  • repository-flow-conventions
    — 用一张方法形态表约定建立在本地数据库与远程 API 之上的 repository——本地读取使用移至 IO dispatcher 的 cold flow,远程读取使用承载 success/error envelope 的 flow,写入使用 withContext。向 repository 添加方法、审查方法形态已经各自漂移的 repository,或屏幕永久停留在 loading 状态且日志中没有任何信息时,查阅此项。
  • cache-then-network
    — 从单一 repository flow 中先立即提供已存储副本,再提供最新副本——只有当两者都未提供任何内容时才发出 error,因为成功 emit 之后的 error 会替换用户正在阅读的有效内容。当每次打开页面都会显示 spinner,尽管一分钟前刚展示过相同数据;或短暂的网络故障清空了原本内容完好的页面时,查阅此项。
  • ttl-keyed-json-cache-lenient-decode
    — 为会逐渐变化的值构建小型 keyed cache——每个 entry 携带获取时刻,并根据 time-to-live 常量执行 isStale 检查;整个 map 作为一个 JSON 字符串存储在 key-value preferences 中;解码采用 lenient 方式并包裹异常,使 schema 变更降级为 cache miss,而不是摧毁所有 entry。缓存 resolved covers、lookups 或 per-key results 而不建数据库表,或添加 model 字段后 cache 完全失效时,查阅此项。
  • generic-paged-db-accumulator
    — 提供一个可复用的 helper,通过 (limit, offset) data-access function 在有界循环中读取整张表,并在首次遇到不足一页的数据时停止——同时说明何时读取全部内容是合理的(export、backup、bulk mapping),以及预示你其实需要真正 query 的 code smell。多个 repository 各自重复编写相同的分页循环,或“读取全部”调用随着表增长而慢于线性增长时,查阅此项。
  • continuation-token-pagination-contract
    — 将返回一页内容和 next-token 的 endpoint 建模为 Flow<Resource<Pair<items, token?>>>——null token 表示结束,调用方只保存 token,并在显示任何内容之前通过有界预取预热前几页。将 opaque-cursor API 接入 repository、列表在一次失败请求后停止加载且再也无法恢复,或一次触发导致分页发出两次时,查阅此项。
  • encoded-continuation-tokens-local-sort
    — 让本地排序和洗牌后的分页复用远程 API 所用的同一个 token 槽位:为 token 添加 mode tag 前缀,并在其后编码 cursor——同时明确拒绝无法识别的前缀,因为被静默忽略的 token 会让 pager 卡在 in-flight 状态,列表再也无法加载。当同一个列表必须从 server cursor 或 local ordering 分页,或 local sorting 导致分页失效且日志中没有任何信息时,查阅此项。
  • order-preserving-section-mapping
    — 将 sectioned API response 按到达顺序映射为 (title, items) list,绝不能将 result[0] 和 result[1] 读入命名字段——有记录显示,登录账户会因此在前面多出一个 section;任何这种偏移都会错误标注其后的每个 section,并悄无声息地丢弃最后一个。假定 section 的集合和顺序也可能因 locale 或 region 而变化,并捕获两份 response 以查明实际情况。为由内容板块(shelves)构成的首页 feed 或浏览页建模,或某些用户的屏幕在正确内容下显示了错误标题时,查阅此项。
  • remote-index-cached-as-rows-with-validator
    — 缓存大型 published index:将其一次性解析为 indexed rows,并使用服务器自己的 ETag 重新检查,使日常 freshness check 只需耗费几百字节——仅在 rows 尚存在时重放已存储的 validator;将解析后为空的 200 视为失败;绝不让失败的检查刷新 timestamp。从 static host 获取可浏览的 catalogue,或 cached index 变空后再也没有补充时,查阅此项。
  • search-over-a-paged-list-queries-the-source
    — 过滤 paging stream 只会搜索已经加载的 pages,因此某项能否被找到取决于用户恰好滚动了多远——search 必须 query store,并作为 sibling overlay 渲染,同时不干扰 paged reader 及其 drag-reorder、in-place removal 和 scroll position。涵盖 debounce 和 minimum-length gate、为什么 escaping 应放在 query 的上一层,以及唯一适合在 memory 中过滤的情况。search 漏掉确定存在的项目、滚动后结果发生变化,或准备向 paged list 接入第二个 data source 时,查阅此项。
  • mirror-local-state-to-a-remote-account
    — 使用 opt-in switch,将本地 flag 镜像到已登录的 remote account——开启时回填所有已标记内容,关闭时停止镜像且有意不予撤销;每个 item 的调用采用三值状态,从而能区分“未尝试”与“失败”。涵盖先写本地并无条件执行、两条路径中哪一条被允许向用户反馈,以及为何回填必须顺序执行。当镜像 flag 与账户状态悄然不一致、用户无法区分失败与 no-op,或准备将设置开关接入 remote write 时,查阅此项。