Support safe offline intent rebasing - #59
Conversation
✅ Deploy Preview for rdlabo-ionic-angular-library ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
| async #currentCompanionRows( | ||
| scope: OfflineScope, | ||
| commands: readonly OfflineCommand[], | ||
| ): Promise<OfflineReplicaRow[]> { | ||
| const keys = new Map( | ||
| commands.flatMap((command) => | ||
| (command.optimisticCompanions ?? []).map((companion) => [ | ||
| `${companion.key.sourceKey}:${JSON.stringify(companion.key.identity)}`, | ||
| companion.key, | ||
| ] as const), | ||
| ), | ||
| ); | ||
| const rows = await Promise.all( | ||
| [...keys.values()].map((key) => this.#repository.getReplicaRow(scope, key.sourceKey, key.identity)), | ||
| ); | ||
| return rows.filter((row): row is OfflineReplicaRow => row !== null); | ||
| } |
There was a problem hiding this comment.
🟡 削除待ちの関連行が「存在しない」と見なされ、再表示されてしまう
付随行の読み出しに削除待ち行を隠すAPIを使っている(getReplicaRow at projects/kit/offline/src/lib/offline-replica-pull.service.ts:270)ため、削除予定の行が製品側の再計算に渡されず、消えたはずの行が作り直される可能性があります。
Impact: ユーザーがオフライン中に削除した明細が、同期後に画面へ復活して見えることがあります。
pending_delete行の可視性とスコープ解決の不一致
IonicOfflineRepository.getReplicaRow は visibility === 'pending_delete' の行を null として返します(projects/kit/offline/src/lib/offline-repository.ts:290)。一方、同等の用途を持つ同期側のヘルパ #getCompanionRow は getReplicaRowIncludingPendingDelete を優先して使い、さらに key.userId / key.scopeId(companion key 自身のスコープ)でスコープを組み立てています(projects/kit/offline/src/lib/offline-sync.service.ts:1196-1201)。
新規の #currentCompanionRows は (1) 削除待ちを隠す getReplicaRow のみを使い、(2) companion key が持つスコープではなく pull の scope を使います。schema.scope === 'user' の集約では related が別 scopeId のコマンドを含み得る(projects/kit/offline/src/lib/offline-replica-pull.service.ts:81)ため、partition 分割されたcompanion行が解決できず欠落します。欠落した行は rebasePendingCommands に渡されないため、製品側は「存在しない」と判断して putRows で再生成し、削除待ちの状態を上書きしてしまいます。
Prompt for agents
offline-replica-pull.service.ts の #currentCompanionRows は、companion 行の取得に this.#repository.getReplicaRow(scope, ...) を使っている。offline-sync.service.ts の #getCompanionRow と同様に、(1) getReplicaRowIncludingPendingDelete があればそれを優先し、(2) スコープは pull の scope ではなく companion.key 自身の userId/scopeId から組み立てるべき。そうしないと削除待ち(pending_delete)の付随行や別 scopeId の付随行が製品の rebasePendingCommands に渡らず、製品が行を再生成して削除意図を打ち消す恐れがある。
Was this helpful? React with 👍 or 👎 to provide feedback.
| const revisionChanged = related.some((command) => command.baseRevision !== change.serverRevision); | ||
| const rebase = revisionChanged | ||
| ? ((await this.#executor.rebasePendingCommands?.( | ||
| related, | ||
| confirmedValues, | ||
| change.serverRevision, | ||
| await this.#currentCompanionRows(scope, related), | ||
| )) ?? null) | ||
| : null; | ||
| this.#assertRebasedValues(related, rebase?.optimisticValues ?? null); | ||
| const conflicted = revisionChanged && rebase === null; | ||
| const rebasedCommands = rebase | ||
| ? related.map((command, index) => ({ | ||
| ...command, | ||
| baseRevision: change.serverRevision, | ||
| optimisticValue: rebase.optimisticValues[index], | ||
| })) | ||
| : []; | ||
| for (const command of rebasedCommands) putCommands.set(command.commandId, command); |
There was a problem hiding this comment.
🟡 競合状態のまま送信できない変更が「同期中」と表示され続ける
すでに競合や拒否で止まっている変更まで新しいサーバー値へ載せ替えてしまう(rebasePendingCommands 呼び出し at projects/kit/offline/src/lib/offline-replica-pull.service.ts:174-183)ため、送信できない状態のまま行だけが同期中の表示になります。
Impact: いつまでも送られない編集が「同期待ち」と表示され、ユーザーが競合に気付けなくなります。
コマンド状態を考慮しないrebaseと行のsyncState決定
related は commands.filter(...) で集約に属する全コマンドを対象にしており、state によるフィルタがありません(projects/kit/offline/src/lib/offline-replica-pull.service.ts:123-127)。そのため直前のpullで state: 'conflict' / lastErrorCode: 'remote_revision' になったコマンドも rebase 対象になります。
rebase が成功すると conflicted は false になり、行は syncState: 'pending'(offline-replica-pull.service.ts:200)になりますが、コマンド側は baseRevision と optimisticValue だけが更新され state/lastErrorCode は 'conflict' のまま残ります(offline-replica-pull.service.ts:184-191)。
送信側は head が pending/retry_wait でなければ集約の送信を打ち切るため(projects/kit/offline/src/lib/offline-sync.service.ts:924,932)、このコマンドは永久に送信されません。また #replicaState では conflict コマンドの行は 'conflict' 表示になるはず(offline-sync.service.ts:1120-1122)で、行の 'pending' 表示とも矛盾します。blocked_auth / rejected のコマンドでも同様です。
Prompt for agents
offline-replica-pull.service.ts の rebase 経路は、集約に属する全コマンド(related)をそのまま rebasePendingCommands に渡し、成功時に baseRevision/optimisticValue のみ更新している。conflict / rejected / blocked_auth のように送信が止まっているコマンドが含まれる場合、rebase 成功で行の syncState が 'pending' になる一方コマンドは送信不能のままとなり、状態が矛盾する。rebase 対象を pending/retry_wait の集約に限定する(それ以外が含まれる場合は従来通り conflict 扱いにする)か、rebase 成功時にコマンド状態と lastErrorCode もリセットするか、いずれかの方針を決めて整合させること。
Was this helpful? React with 👍 or 👎 to provide feedback.
| export interface OfflinePendingRebase { | ||
| /** Recomputed optimistic values in the original durable FIFO order. */ | ||
| optimisticValues: readonly unknown[]; | ||
| /** Product-owned companion rows rematerialized from the new confirmed value. */ | ||
| putRows?: readonly OfflineReplicaRow[]; | ||
| removeRows?: readonly OfflineReplicaRowKey[]; | ||
| } |
There was a problem hiding this comment.
🟡 新しく公開された型にドキュメントコメントが無い
公開エクスポートされる型定義にJSDocが付与されていない(OfflinePendingRebase at projects/kit/offline/src/lib/offline-command-executor.ts:60)ため、リポジトリ規約に違反しています。
Impact: 公開APIのドキュメント品質が規約どおりに保たれません。
AGENTS.md の要求
AGENTS.md「When modifying this repo」3項に「Every public class, function, and type must have a JSDoc comment.」とあります。OfflinePendingRebase は projects/kit/offline/src/public-api.ts:7 経由で公開されますが、インターフェース本体にJSDocがありません(メンバの removeRows にもコメントがありません)。
| export interface OfflinePendingRebase { | |
| /** Recomputed optimistic values in the original durable FIFO order. */ | |
| optimisticValues: readonly unknown[]; | |
| /** Product-owned companion rows rematerialized from the new confirmed value. */ | |
| putRows?: readonly OfflineReplicaRow[]; | |
| removeRows?: readonly OfflineReplicaRowKey[]; | |
| } | |
| /** Result of a product-owned rebase of pending intents onto a newer confirmed revision. */ | |
| export interface OfflinePendingRebase { | |
| /** Recomputed optimistic values in the original durable FIFO order. */ | |
| optimisticValues: readonly unknown[]; | |
| /** Product-owned companion rows rematerialized from the new confirmed value. */ | |
| putRows?: readonly OfflineReplicaRow[]; | |
| /** Product-owned companion rows that no longer exist after the rebase. */ | |
| removeRows?: readonly OfflineReplicaRowKey[]; | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Why
Revision-sensitive writes and commutative deltas cannot share the same conflict rule. Previously, every unrelated server revision forced a pending delta into conflict and could lead users to discard a valid quantity intent. A product reducer can now recompute optimistic values from the new confirmed value without weakening idempotency or letting product code rewrite durable command payloads.
Verification