Skip to content

Fix offline repository invariants - #55

Merged
rdlabo merged 2 commits into
mainfrom
fix/offline-root-invariants
Aug 11, 2026
Merged

Fix offline repository invariants#55
rdlabo merged 2 commits into
mainfrom
fix/offline-root-invariants

Conversation

@rdlabo

@rdlabo rdlabo commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What changed

  • serialize Web replica partition build, mutation, and clear operations through one write lane
  • make prepared-command replacement atomic with optimistic replica changes
  • preserve reconciliation and command ambiguity invariants across retry, discard, restart, and account changes
  • add deterministic race and repository regression coverage

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

  • Kit offline tests: 43 files / 715 tests
  • Kit production build
  • independent manager review: APPROVE
  • independent acceptance review: APPROVE

Integration order

Merge and release this PR before the dependent winecode app PR.

@netlify

netlify Bot commented Aug 11, 2026

Copy link
Copy Markdown

Deploy Preview for rdlabo-ionic-angular-library ready!

Name Link
🔨 Latest commit a9b4088
🔍 Latest deploy log https://app.netlify.com/projects/rdlabo-ionic-angular-library/deploys/6a7b4a9c170bb300084110d0
😎 Deploy Preview https://deploy-preview-55--rdlabo-ionic-angular-library.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@rdlabo
rdlabo marked this pull request as ready for review August 11, 2026 16:17

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

putRows: [optimisticRow, ...(replicaTransaction?.putRows ?? [])],
removeRows: replicaTransaction?.removeRows,
putCommands: [command],
removeCommandIds: replaced ? [replaced.commandId] : undefined,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 操作の差し替え時に付随データの取り消しが行われず、古い仮の値が残り続ける

差し替え対象の操作が削除される (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) は #enqueuereplaced を渡すだけで、#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 の差し替えを拒否するなどの対処を検討してほしい。
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +264 to +268
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 同じ対象に未送信の操作が残っていると、差し替えた操作が最後尾へ回り送信順が入れ替わる

差し替えで作られた新しい操作は常に最新時刻で登録され (createdAt: await this.#nextCommandCreatedAt(userId) at projects/kit/offline/src/lib/offline-sync.service.ts:344) 元の操作の位置ではなく待ち行列の最後尾へ置かれるため、同じ対象に残っていた後続操作が先に送信されます。

Impact: 同じデータに対する変更がユーザーの意図した順序と異なる順序でサーバへ適用されます。

aggregate 内の順序と楽観行の上書き

command は compareOfflineCommandscreatedAt 昇順)で並び、#sendAggregate は aggregate 単位で先頭から順に送信します。conflict 状態の command は #eligibleAggregateGroupsprojects/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 で追加するため、順序は [旧, 後続] から [後続, 新] へ入れ替わります。さらに #enqueueoptimisticRow.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 を引き継いで順序位置を保持するかを検討してほしい。
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@rdlabo
rdlabo merged commit f3fe357 into main Aug 11, 2026
12 checks passed
@rdlabo
rdlabo deleted the fix/offline-root-invariants branch August 11, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant