Skip to content

Unify offline rebase and rebaseline pull semantics - #61

Merged
rdlabo merged 11 commits into
mainfrom
feat/offline-atomic-pull-projection
Aug 12, 2026
Merged

Unify offline rebase and rebaseline pull semantics#61
rdlabo merged 11 commits into
mainfrom
feat/offline-atomic-pull-projection

Conversation

@rdlabo

@rdlabo rdlabo commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add an opt-in aggregate reducer for rebasing commutative offline intents onto a newer confirmed revision
  • make rebaselineRequired part of the Kit wire and pull-page contracts
  • atomically apply rebaseline cleanup, authoritative base rows, rebased pending projections, product-derived local-only rows, commands, and cursor
  • preserve pending companions across user-scoped partitions with canonical replica identity
  • reject projector writes outside the current scope or to synchronized base entities

Why

Conflict rebase and cursor rebaseline both modify the same pull transaction. Shipping them independently would leave an untested merge boundary. This stacked PR includes #59 and proves both invariants together: revision-sensitive writes still conflict, commutative intent may be safely replayed, and an expired cursor cannot split confirmed cleanup from the first snapshot commit.

Verification

  • focused pull and contract tests: 56/56
  • ESLint: pass
  • git diff --check: pass

@netlify

netlify Bot commented Aug 12, 2026

Copy link
Copy Markdown

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

Name Link
🔨 Latest commit 15bc106
🔍 Latest deploy log https://app.netlify.com/projects/rdlabo-ionic-angular-library/deploys/6a7c35992fcce60008073b6f
😎 Deploy Preview https://deploy-preview-61--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.

@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 4 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment on lines +84 to +86
if (page.rebaselineRequired) {
removeRows.push(...(await this.#confirmedRowsForRebaseline(scope, scopeCommands)));
}

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.

🔴 再ベースライン時に取得したばかりの最新データが同じ処理内で消えてしまう

サーバーからの作り直し指示を受けたとき、削除対象に選んだ古いデータの一覧に、同じ取り込みで書き直された最新データも含まれたまま渡される(removeRows.push(...) at projects/kit/offline/src/lib/offline-replica-pull.service.ts:85)ため、書き込みの後に削除が行われて最新データが消えます。
Impact: 再取得直後に本来残るはずの既存データや派生表示用データが端末から消え、しおり位置(カーソル)は進むため再取得もされません。

書き込み順序(putRows→removeRows)と鍵の衝突

#confirmedRowsForRebaselinesyncState === 'confirmed' の全行を削除対象として収集します(projects/kit/offline/src/lib/offline-replica-pull.service.ts:228-243)。その後の変更適用ループでは、同じ行が更新対象になると putRows{...existing, identity: existing.identity} として積まれます。最終的に両者が同一トランザクションへ渡されますが(projects/kit/offline/src/lib/offline-replica-pull.service.ts:210-216)、Web 実装は putRows を先に適用してから removeRows を削除し(projects/kit/offline/src/lib/offline-repository.ts:678-689)、SQLite 実装も同様の順序です(projects/kit/offline/src/lib/sqlite-offline-repository.ts:411-420)。そのため、スナップショットページで再確認された既存行、および projector が再構築した既存の localOnly 派生行(これらも confirmed なら削除対象に入る)が削除されます。テストでは削除対象 remoteId=10 と新規 remoteId=42、新規 view-42 を使っているため衝突が起きず検出されません。

修正案としては、putRows(projector 分を含む)に含まれる行キーを removeRows から除外する、あるいはリポジトリ側で remove を put より前に適用することが考えられます。

Prompt for agents
再ベースライン(rebaselineRequired)で収集した confirmed 行の削除リストと、同じページで書き直される putRows(projector 由来の localOnly 行を含む)が同一の行キーで衝突する。リポジトリ実装(IonicOfflineRepository / SqliteOfflineRepository)はいずれも transactReplica 内で putRows を先に適用してから removeRows を削除するため、最新化されたばかりの行が削除されてしまう。OfflineReplicaPullService.pull で最終的な putRows を組み立てた後に、その行キー集合に含まれるものを removeRows から除外する(キー比較は canonicalOfflineReplicaIdentity など既存の正規化と同じ方式を使う)か、リポジトリ側で remove→put の順に変更することを検討する。既存テストは衝突しないデータを使っているため、同一 identity が snapshot ページで更新されるケースの回帰テストも追加すること。
Open in Devin Review

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

Comment on lines +245 to +247
#rowKey(row: OfflineReplicaRowKey): string {
return `${row.userId}:${row.scopeId}:${row.sourceKey}:${JSON.stringify(row.identity)}`;
}

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.

🔴 未送信の変更に紐づく派生データが再ベースラインで誤って削除される

未送信の変更が守るべき派生データかどうかを判定する鍵(JSON.stringify(row.identity) at projects/kit/offline/src/lib/offline-replica-pull.service.ts:246)が、既存実装の正規化方式と異なり付随情報まで含めて比較するため、同じデータでも別物とみなされて削除されます。
Impact: 送信待ちの操作に伴って表示していた関連データが再取得時に消え、画面表示が壊れます。

identity 正規化の不一致

コードベース全体では行 identity の同一性は canonicalOfflineReplicaIdentityprojects/kit/offline/src/lib/offline-identity.ts:73-83)で判定され、generated identity は localId のみで比較され remoteId は無視されます。しかし新規の #rowKeyprojects/kit/offline/src/lib/offline-replica-pull.service.ts:245-247)は JSON.stringify(row.identity) を使うため、{kind:'generated', localId:'x', remoteId:null} として保存されたコマンドの companion キーと、その後 remoteId が割り当てられたリポジトリ上の行が一致せず、保全集合から漏れて削除されます。さらに JSON 文字列化はプロパティ順やキー順の差にも依存します。offline-sync.service.ts:680-682#replicaRowKey と同じ正規化方式に揃えるべきです。

Suggested change
#rowKey(row: OfflineReplicaRowKey): string {
return `${row.userId}:${row.scopeId}:${row.sourceKey}:${JSON.stringify(row.identity)}`;
}
#rowKey(row: OfflineReplicaRowKey): string {
return `${canonicalOfflinePrincipalId(row.userId)}:${row.scopeId}:${row.sourceKey}:${canonicalOfflineReplicaIdentity(row.identity)}`;
}
Open in Devin Review

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

Comment on lines +84 to +86
if (page.rebaselineRequired) {
removeRows.push(...(await this.#confirmedRowsForRebaseline(scope, scopeCommands)));
}

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.

🟡 別パーティションの未送信操作に紐づく共有データが再ベースラインで消える

再ベースラインで守るべきデータを決めるとき、現在の区画の未送信操作だけを参照している(#confirmedRowsForRebaseline(scope, scopeCommands) at projects/kit/offline/src/lib/offline-replica-pull.service.ts:85)ため、利用者単位で共有されるデータに紐づく他区画の未送信操作分が守られず削除されます。
Impact: 別の区画で行った未送信の操作に伴う表示データが、再取得時に消えます。

scopeCommands と userCommands の使い分け

同じ pull 処理内では、schema.scope === 'user' のエンティティに対しては userCommandsgetCommandsForUser)を使ってコマンドを突き合わせています(projects/kit/offline/src/lib/offline-replica-pull.service.ts:71-81)。一方 #confirmedRowsForRebaseline に渡されるのは scopeCommands のみです。また削除候補は getReplicaRows(scope, entity.sourceKey) により user スコープのエンティティでは全区画の行が返るため、他区画のコマンドが持つ optimisticCompanions 行も削除対象に入り得ます。user スコープのエンティティについては userCommands を保全集合に含めるべきです。

Open in Devin Review

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

Comment on lines +56 to +57
/** The server can no longer continue this cursor and requires a confirmed-state snapshot rebuild. */
rebaselineRequired?: boolean;

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.

🟡 サーバー応答の変換関数を通すと作り直し指示を型として渡せない

作り直し指示の項目が取り込みページの型にだけ追加され、バックエンド応答用の型には追加されていない(rebaselineRequired?: boolean at projects/kit/offline/src/lib/offline-replica-puller.ts:57)ため、共有の変換関数を使う製品はこの指示を型安全に伝えられません。
Impact: 共通変換を利用しているアプリでは、サーバーからの作り直し指示を渡すのに型回避が必要になります。

OfflineReplicaWirePullPage との不整合

normalizeOfflineReplicaPullPageprojects/kit/offline/src/lib/offline-replica-puller.ts:89-104)は OfflineReplicaWirePullPage を受け取り OfflineReplicaPullPage を返しますが、入力側インターフェース(projects/kit/offline/src/lib/offline-replica-puller.ts:77-83)には rebaselineRequired がありません。実行時はスプレッドで通過しますが、型上は宣言できないため、as unknown などのキャストが必要になります。入力型にも省略可能な rebaselineRequired を追加するのが自然です。

Open in Devin Review

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

@rdlabo rdlabo changed the title Apply offline rebaseline projections atomically Unify offline rebase and rebaseline pull semantics Aug 12, 2026
@rdlabo
rdlabo merged commit 0354cfe into main Aug 12, 2026
12 checks passed
@rdlabo
rdlabo deleted the feat/offline-atomic-pull-projection branch August 12, 2026 09:01
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