Fix offline repository invariants - #55
Conversation
✅ Deploy Preview for rdlabo-ionic-angular-library ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
| putRows: [optimisticRow, ...(replicaTransaction?.putRows ?? [])], | ||
| removeRows: replicaTransaction?.removeRows, | ||
| putCommands: [command], | ||
| removeCommandIds: replaced ? [replaced.commandId] : undefined, |
There was a problem hiding this comment.
🟡 操作の差し替え時に付随データの取り消しが行われず、古い仮の値が残り続ける
差し替え対象の操作が削除される (removeCommandIds: [replaced.commandId] at projects/kit/offline/src/lib/offline-sync.service.ts:446) 際に、その操作が書き込んだ付随データを元の値へ戻す処理が行われないため、誰も責任を持たない古い仮の値が端末内に残り続けます。
Impact: 画面に、サーバへ送られることのない古い暫定表示が恒久的に残ります。
破棄経路との非対称性 — optimisticCompanions のロールバック欠落
discard は #discardCommands (projects/kit/offline/src/lib/offline-sync.service.ts:955-1015) を通り、削除する command の optimisticCompanions について before/confirmedValues を用いた companion row の巻き戻し(putRows / removeRows)を同一 transaction で行います。
一方 replacePrepared (projects/kit/offline/src/lib/offline-sync.service.ts:255-270) は #enqueue に replaced を渡すだけで、#enqueue の transaction は putRows: [optimisticRow, ...prepared.replicaTransaction.putRows] と removeCommandIds: [replaced.commandId] のみを行います。差し替え後の prepared transaction が同じ companion row を再度書き換えない場合、旧 command が適用した companion row の楽観値はそのまま残り、かつ旧 command が消えているため以後 #companionsAfterDiscard からも復元対象になりません(projects/kit/offline/src/lib/offline-sync.service.ts:1040-1060)。
Prompt for agents
replacePrepared (offline-sync.service.ts) で差し替え対象 command を removeCommandIds で消す際、その command が持つ optimisticCompanions の巻き戻しが行われない。discard 経路 (#discardCommands / #companionsAfterDiscard) は companion row を before / confirmedValues に基づいて復元しているため非対称になっている。置き換え後の prepared transaction が同じ companion key を上書きしないケースでは、所有者のいない楽観値が replica に残り、以後どの経路でも復元されない。#enqueue の replaced 分岐で、旧 command の companion のうち新 command が再度カバーしないものについて discard と同じ復元 row / 削除 key を同一 transaction に含める、あるいは replacePrepared 側で companion を持つ command の差し替えを拒否するなどの対処を検討してほしい。
Was this helpful? React with 👍 or 👎 to provide feedback.
| const replaced = (await this.#readKnownCommands()).find((command) => command.commandId === commandId); | ||
| if (!replaced) throw new Error(`Offline command ${commandId} no longer exists.`); | ||
| this.#assertDiscardable([replaced]); | ||
| const prepared = await prepare(this.#repository); | ||
| return this.#enqueue(prepared.request, options, generation, prepared.replicaTransaction, replaced); |
There was a problem hiding this comment.
🟡 同じ対象に未送信の操作が残っていると、差し替えた操作が最後尾へ回り送信順が入れ替わる
差し替えで作られた新しい操作は常に最新時刻で登録され (createdAt: await this.#nextCommandCreatedAt(userId) at projects/kit/offline/src/lib/offline-sync.service.ts:344) 元の操作の位置ではなく待ち行列の最後尾へ置かれるため、同じ対象に残っていた後続操作が先に送信されます。
Impact: 同じデータに対する変更がユーザーの意図した順序と異なる順序でサーバへ適用されます。
aggregate 内の順序と楽観行の上書き
command は compareOfflineCommands(createdAt 昇順)で並び、#sendAggregate は aggregate 単位で先頭から順に送信します。conflict 状態の command は #eligibleAggregateGroups(projects/kit/offline/src/lib/offline-sync.service.ts:694-707)で group 全体を停止させるため、conflict command の後ろに pending command が積まれた状態は通常に発生します。
replacePrepared (projects/kit/offline/src/lib/offline-sync.service.ts:255-270) はその conflict command を削除し、新 command を #nextCommandCreatedAt により最新 createdAt で追加するため、順序は [旧, 後続] から [後続, 新] へ入れ替わります。さらに #enqueue は optimisticRow.values を新 command の optimisticValue で上書きするため、後続 command の楽観投影も失われます。replacePrepared 側で「差し替え対象が当該 aggregate の唯一の command であること」を検証するか、createdAt を旧 command のものに引き継ぐ必要があります。
Prompt for agents
replacePrepared は差し替え対象 command と同じ aggregate に後続 command が残っている場合を考慮していない。新 command は #nextCommandCreatedAt により最新の createdAt を取得するため、削除した旧 command の位置ではなく待ち行列の最後尾に入り、後続 command が先に送信される。加えて #enqueue が optimistic row の values を新 command の optimisticValue で上書きするため、後続 command の楽観投影が失われる。replacePrepared で対象 aggregate に他の command が存在する場合を明示的に拒否するか、旧 command の createdAt を引き継いで順序位置を保持するかを検討してほしい。
Was this helpful? React with 👍 or 👎 to provide feedback.
What changed
Why
Offline correctness previously depended on timing between canonical rows, partition indexes, command replacement, and cleanup. A process interruption or account/scope transition could leave a ready partition or Outbox command inconsistent with its canonical state.
This change moves those operations behind repository-level invariants so product apps do not need endpoint-specific recovery code.
Validation
Integration order
Merge and release this PR before the dependent winecode app PR.