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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user