feat: sync ETag concurrency + fix chunk-transfer hang + sync overlay + auto-VACUUM

Sync optimistic concurrency (ETag/If-Match)
- webdav GET captures the response ETag; PUT sends it back as If-Match so
  the server rejects (412) our write when another device changed the file
  between our pull and push. A 412 re-runs the whole pull→merge→push
  (bounded to 3) so the other device's changes are folded in instead of
  clobbered. Servers without ETags → empty etag → no If-Match → falls
  back to last-write-wins (no regression). onWebdavResult gained a 4th
  etag arg.

Chunked webdav PUT (big vaults no longer black-screen on sync)
- The whole encrypted snapshot base64'd into a single cmd://webdav/put URL
  blew past WebView2's cap → black screen once the vault grew (20MB of
  attachments). PUT bodies now stream through the file/chunk transport and
  commit via a new webdav/put-commit (reads the accumulated buffer).

Chunk-transfer hang fix (root cause of the stuck "Preparing…" sync)
- All chunked transfers (saveFile/writeFile/webdav PUT) share one
  reqId-keyed resolver. A resolved chunk's stale 30s timeout would later
  delete the CURRENT chunk's resolver and fire the wrong res(), leaving
  that chunk's await pending forever. Extracted a single _streamChunks()
  helper whose ack CLEARS the pending timeout, so resolvers stay strictly
  one-at-a-time. Also fixed _webdavCall referencing the Bridge-local cmd()
  from module scope (latent ReferenceError).

Sync busy overlay
- syncStatus() now drives the global busy overlay too, so a running sync
  blocks stray clicks (e.g. the auto-backup "Choose…" picker) and reads
  like the manual backup. The account-mismatch confirm hideBusy()s first
  so it's visible above the overlay.

Auto-VACUUM (reclaim space after deleting large attachments)
- SQLite never shrinks the file on DELETE, so deleting big attachments
  left vault.db bloated (35MB for 11 tiny entries). DB.CompactIfBloated
  VACUUMs when >20% of pages are free AND >~2MB is reclaimable — called on
  startup and after each attachment delete. A healthy small vault pays
  nothing. (Verified: 35MB → 695KB after the deletes.)

Rebuild: BuildAssets + F9 (UMainForm + PM.Database + PM.Handler.Attachments).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-04 00:02:25 +01:00
parent b367d031b5
commit ac909f4f09
6 changed files with 195 additions and 57 deletions
+39
View File
@@ -33,6 +33,11 @@ type
destructor Destroy; override;
procedure Lock;
procedure Unlock;
// VACUUM the file when a large fraction of its pages are free (SQLite
// never shrinks on DELETE — deleting big attachments leaves the file
// bloated). No-op when the free ratio is low, so the common case pays
// nothing. Caller must NOT hold the lock or be in a transaction.
procedure CompactIfBloated;
property Connection: TFDConnection read FConn;
property DBPath: string read FDBPath;
end;
@@ -61,6 +66,10 @@ begin
CreateSchema;
ApplyMigrations;
CleanupExpired;
// Reclaim space left behind by previously-deleted large rows
// (attachments especially). Only actually rewrites the file when it's
// meaningfully bloated, so a healthy small vault opens instantly.
CompactIfBloated;
end;
destructor TPMDatabase.Destroy;
@@ -70,6 +79,36 @@ begin
inherited;
end;
procedure TPMDatabase.CompactIfBloated;
var
LQ: TFDQuery;
LFree, LTotal: Int64;
begin
FLock.Enter;
try
LFree := 0; LTotal := 0;
LQ := TFDQuery.Create(nil);
try
LQ.Connection := FConn;
LQ.SQL.Text := 'PRAGMA freelist_count';
LQ.Open; if not LQ.IsEmpty then LFree := LQ.Fields[0].AsLargeInt;
LQ.Close;
LQ.SQL.Text := 'PRAGMA page_count';
LQ.Open; if not LQ.IsEmpty then LTotal := LQ.Fields[0].AsLargeInt;
LQ.Close;
finally
LQ.Free;
end;
// Rewrite only when >20% of the pages are free AND there's at least a
// few MB to reclaim (avoids churning a tiny vault). VACUUM can't run
// inside a transaction, so this must be called outside one.
if (LTotal > 0) and (LFree * 5 > LTotal) and (LFree > 512) then
FConn.ExecSQL('VACUUM');
finally
FLock.Leave;
end;
end;
procedure TPMDatabase.Lock;
begin
FLock.Enter;