Commit graph

52 commits

Author SHA1 Message Date
Johannes Millan
d32f7037a3 fix(sync): preserve own vector clock counter across full-state op resets
When a full-state op (SYNC_IMPORT/BACKUP_IMPORT) arrived from another
client, mergeRemoteOpClocks reset the local clock to a "minimal" form
but only preserved the current client's counter from the incoming op's
clock. If the current client had issued ops (e.g. GLOBAL_CONFIG) not
reflected in the incoming full-state op's clock, its counter was dropped,
causing subsequent ops to reuse the same counter value. Downstream clients
then saw these ops as EQUAL (duplicate) and skipped them silently.

Fix: take max(mergedClock[clientId], currentClock[clientId]) when
rebuilding the clock after a full-state op reset.

Also add __SP_E2E_BLOCK_WS_DOWNLOAD flag to WsTriggeredDownloadService
to allow E2E tests to block automatic WS-triggered downloads during
concurrent conflict scenarios.

Fix archive conflict test by blocking WS downloads on Client A during
the concurrent edit phase so it doesn't auto-receive B's rename via
WebSocket before archiving (restoring the intended conflict scenario).

Fix LWW singleton test to assert convergence rather than specific winner.
Fix renameTask helper to avoid Playwright/Angular re-render races.
Fix shepherd.js import paths that broke the Angular dev server build.
2026-04-01 15:41:13 +02:00
dependabot[bot]
f9ba198302
chore(deps): bump vitest from 3.2.4 to 4.1.2 (#7036)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.2.4 to 4.1.2.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.2/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 13:39:16 +02:00
Johannes Millan
861425fd28 refactor(sync): improve vector clock implementation quality
- Replace O(N) Prisma findMany with PostgreSQL SQL aggregate
  (jsonb_each_text + GROUP BY + MAX) for snapshot vector clock
  computation, avoiding loading all ops' clocks into Node.js memory
- Add defensive regex filter (^[0-9]+$) for non-numeric JSONB values
- Fix || to ?? for nullish coalescing in vector clock value lookups
- Remove unreachable CONCURRENT check in compareVectorClocks
- Remove unused VectorClockMetrics interface and measureVectorClock
- Add 7 new unit tests for SQL aggregate path verification
- Add 10 integration tests running actual SQL against PostgreSQL
2026-03-17 13:59:40 +01:00
Johannes Millan
454fefcb04
test(sync): add vector clock pruning edge case tests (#6506)
Add tests for previously untested vector clock pruning scenarios:

- Multi-preserve-ID pruning (two low-counter IDs both preserved)
- Overlapping/duplicate preserve IDs (Set deduplication)
- Preserve IDs missing from clock (silent skip, no crash)
- Snapshot vector clock aggregation + pruning integration
- Post-snapshot comparison correctness after pruning
- Server sanitizeVectorClock counter cap at 100M
- DoS cap at 2.5x MAX (reject, not prune)
- Invalid inputs (null, arrays, empty keys, long keys, negative/float values)

https://claude.ai/code/session_01GdsbKoo8eax394j2UyWUu1

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-13 12:42:36 +01:00
Johannes Millan
43b5808a3f fix(sync): harden vector clock sanitization and improve E2E test robustness
- Cap sanitizeVectorClock values at 100M instead of MAX_SAFE_INTEGER (prevents adversarial clock inflation)
- Add early-exit optimization in compareVectorClocks for CONCURRENT detection
- Add test.setTimeout(180000) to token-expiry and lastseq-preservation E2E tests
- Remove dead code (unreachable guard) in compaction E2E test
- Extract shared config helpers (navigateToMiscSettings, toggleSetting, etc.) into e2e/utils/config-helpers.ts
- Fix Client B cleanup in provider-switch test (move to finally block)
- Add clarifying comments for raw SQL schema coupling, CORS header, and mergeRemoteOpClocks caller contract
- Extract enforceStorageQuota helper in sync routes to reduce duplication
- Add conflictType to conflict detection for structured error handling
- Add MAX_CLEANUP_ITERATIONS guard to freeStorageForUpload
2026-02-12 16:27:56 +01:00
Johannes Millan
5b7c9bdfb7 test(sync): add comprehensive SuperSync test coverage
Add 7 E2E tests (lastSeq preservation, token expiry recovery, cascade
delete, compaction resilience, global config edge cases, provider switch,
multi-tab), 2 server unit tests (replaceToken expiry regression,
middleware auth), 2 client integration tests (IndexedDB error recovery,
schema version sync), and locale-independent sort edge cases for vector
clocks.
2026-02-12 16:27:56 +01:00
Johannes Millan
c70ced204e fix(sync): fix stale comments, doc refs, and preserve lastSeq on clean-slate
Remove references to deleted docs/ai/ files, update stale comments about
protectedClientIds and pruning-aware comparison to reflect current REPLACE
semantics, fix MAX=30→20 heading, client_0..29→19 comment, use ?? over ||,
preserve lastSeq in server clean-slate path to prevent sequence reuse, and
add clarifying comments for mock limitations and validation guards.
2026-02-12 16:27:56 +01:00
Johannes Millan
ea91e02b1a test(sync): add regression tests and update stale doc reference
- Add deterministic tie-breaking test for vector clock pruning with
  equal counters (shared-schema)
- Remove last isLikelyPruningArtifact reference from entity versioning doc
2026-02-12 16:27:55 +01:00
Johannes Millan
3f99e3773c fix(sync): fix replaceToken expiry, locale-independent sort, and stale comments
- Use JWT_EXPIRY_PASSKEY (7d) for replaceToken instead of 365d magic link
  expiry — token replacement is a security action, shorter lifetime is safer
- Replace localeCompare with locale-independent comparator in vector clock
  tie-breaking to ensure deterministic behavior across environments
- Fix 5 additional stale MAX=30 references in docs and tests (now 20)
- Update authentication.md to reflect dual JWT expiry tiers
- Clean up isLikelyPruningArtifact references in docs and LEGACY_MAX in tests
2026-02-12 16:27:55 +01:00
Johannes Millan
0b73461690 fix(sync): fix vector clock comparison parity and validation consistency
- Fix client/server comparison divergence: remove isVectorClockEmpty
  short-circuit that caused {} vs {a:0} to return LESS_THAN on client
  but EQUAL on shared impl. Now delegates all comparisons to shared.
- Align server value cap with client: raise from 10M to MAX_SAFE_INTEGER
  to prevent silent clock entry stripping on long-term usage.
- Make client validation require integers (Number.isInteger) matching
  server-side validation.
- Add deterministic tie-breaking (localeCompare) to pruning sort for
  equal counter values.
- Fix stale comments: MAX references and CLAUDE.md DoS cap (5x→2.5x).
2026-02-12 16:27:55 +01:00
Johannes Millan
f37280e082 refactor(sync): reduce MAX_VECTOR_CLOCK_SIZE from 30 to 20
Lower the cap to leave headroom for future increases and surface
size-related edge cases earlier. All pruning logic is MAX-agnostic
so this is a safe constant change with documentation updates.
2026-02-12 16:27:55 +01:00
Johannes Millan
29541951a3 refactor(sync): increase MAX_VECTOR_CLOCK_SIZE from 10 to 30 and remove defense layers
At MAX=10, pruning triggered frequently enough (11+ unique client IDs from
reinstalls/new browsers) to require 4 defense layers compensating for
information loss: pruning-aware comparison, protected client IDs with
migration, isLikelyPruningArtifact heuristic, and same-client check.

At MAX=30, pruning almost never triggers (needs 31+ unique client IDs).
A 30-entry clock is ~500 bytes — negligible bandwidth. This allows removing
most defense layers while keeping two cheap backward-compat checks for old
10-entry pruned data still on servers.

Removed:
- Pruning-aware mode in compareVectorClocks (standard comparison now)
- Protected client IDs mechanism (storage, migration, preservation)
- selectProtectedClientIds function
- Clock normalization in SyncImportFilterService

Kept temporarily (backward compat with old 10-entry data):
- isLikelyPruningArtifact with LEGACY_MAX=10
- Same-client check (always mathematically correct)
2026-02-12 16:27:55 +01:00
Johannes Millan
9e11b326bd test(sync): add integration tests for vector clock pruning pipeline
Cover the full round-trip of limitVectorClockSize → compareVectorClocks →
isLikelyPruningArtifact with realistic MAX-entry clocks to guard against
regressions in the layered pruning heuristics.
2026-02-10 20:01:26 +01:00
Johannes Millan
8937163513 fix(sync): remove client-side vector clock pruning and fix pruning-aware comparison
Client-side pruning in OperationLogEffects dropped client IDs from
vector clocks (12→10 entries). Combined with the >= threshold in
compareVectorClocks' bothPossiblyPruned check, this caused false
CONCURRENT results and an infinite sync rejection loop.

- Remove limitVectorClockSize call from OperationLogEffects (server
  already prunes AFTER comparison but BEFORE storage per arch doc #13)
- Change bothPossiblyPruned from >= to === MAX_VECTOR_CLOCK_SIZE
  (a clock exceeding MAX was never pruned by limitVectorClockSize)
- Update test expectations and comments in both shared-schema and
  client-side specs to reflect the corrected behavior
2026-02-09 17:55:12 +01:00
Johannes Millan
2a6de0e41f fix(sync): remove dead code, add defensive checks, and clean up vector clock logic
- Remove redundant subset condition in compareVectorClocks
- Add clarifying comment for conservative return in hasVectorClockChanges
- Batch per-key verbose logs into single summary log
- Add defensive warning for negative startingSeq in file-based sync
- Extract magic timeout constant in E2E test
- Update stale doc date
2026-02-02 17:15:13 +01:00
Johannes Millan
192ec62d4a fix(sync): harden vector clock comparison and fix docs
- Replace fragile VectorClockComparison[result] enum lookup with safe cast
- Return CONCURRENT instead of EQUAL when only one side has non-shared
  keys in pruning-aware mode (safe direction: triggers LWW instead of
  silent skip)
- Fix docs claiming clocks "reset" at MAX_SAFE_INTEGER (they throw)
2026-01-30 20:05:48 +01:00
Johannes Millan
c5409bbd25 fix(sync): add server pruning log, harden test coverage for vector clocks
- Add Logger.warn() in ValidationService when oversized vector clocks
  are pruned server-side, improving observability for buggy clients
- Add tests for hasVectorClockChanges (previously zero coverage)
- Add symmetric LESS_THAN test for pruning-aware comparison mode
- Export MAX_LWW_REUPLOAD_RETRIES constant to eliminate magic number
  coupling between sync-wrapper service and its test
- Add test verifying uploading client ID is preserved during server-side
  clock pruning
- Add test for limitVectorClockSize when preserveClientIds exceeds MAX
2026-01-30 19:50:30 +01:00
Johannes Millan
495f0fe0d3 fix(sync): improve vector clock pruning awareness and harden tests
Make hasVectorClockChanges pruning-aware by checking clock size before
logging missing keys — downgrade to verbose when pruning is likely,
warn when corruption is likely. Add return value assertion to LWW retry
exhaustion test, asymmetric pruning test cases, server-side pruning
tradeoff documentation, and fix stale "max 50" in docs.
2026-01-30 19:35:51 +01:00
Johannes Millan
b113a2d7dd fix(sync): return CONCURRENT instead of EQUAL when pruned clocks have non-shared keys
When both vector clocks are at MAX size (pruning-aware mode) and shared
keys compare as equal, non-shared keys on both sides indicate genuinely
different causal histories. Returning EQUAL caused silent data loss by
skipping operations as duplicates. Returning CONCURRENT safely triggers
LWW conflict resolution instead.

Also fixes server snapshot pruning to preserve the requesting client's
ID and the snapshot author's client ID when limiting vector clock size.
2026-01-30 19:21:45 +01:00
Johannes Millan
002c37e054 fix(sync): guard empty intersection in vector clock comparison and fix LWW return value
Return CONCURRENT instead of EQUAL when two max-size clocks share no
keys (independent client populations). Fix misleading SyncStatus.InSync
return when LWW retry exhaustion leaves pending ops. Document known
limitation of size-based pruning heuristic. Add missing edge case tests.
2026-01-30 19:17:02 +01:00
Johannes Millan
fd3d06c5ff test(sync): add tests for vector clock pruning and LWW retry loop fix
Cover all 4 changes from cb36c09538: shared-schema vector clock tests
(22 Vitest), sync wrapper LWW retry loop limit tests (4 Jasmine),
client vector clock pruning-aware comparison tests (3 Jasmine), and
E2E tests for heavy LWW conflict, TAG:TODAY convergence, and
multi-client vector clock consistency (3 Playwright).
2026-01-30 19:04:44 +01:00
Johannes Millan
c268b1a9a9 fix(sync): harden vector clock pruning and post-review sync fixes
Address issues found by code review agents after the LWW loop fix
(cb36c09538):

- Enforce MAX_VECTOR_CLOCK_SIZE on server upload to prevent oversized
  clocks from buggy/adversarial clients
- Limit aggregated snapshotVectorClock in download service to prevent
  unbounded growth with many clients
- Set UNKNOWN_OR_CHANGED status when LWW retry cap is exhausted instead
  of misleadingly reporting IN_SYNC
- Wrap startPostSyncCooldown() in try-catch so endApplyingRemoteOps()
  always runs even on failure
- Cap limitVectorClockSize output at MAX even when preserved IDs exceed
  the limit
- Update stale doc comments referencing MAX_VECTOR_CLOCK_SIZE=8 to 10
2026-01-30 18:40:54 +01:00
Johannes Millan
cb36c09538 fix(sync): prevent infinite LWW auto-resolve loop for TAG:TODAY
Three root causes addressed:

1. Pruning asymmetry: Different clients preserve their own clientId
   during vector clock pruning, producing different clock shapes.
   When compared, pruned-away keys default to 0, causing false
   CONCURRENT verdicts and infinite rejection loops. Fix: when both
   clocks are at MAX_VECTOR_CLOCK_SIZE, compare only shared keys
   (intersection) instead of the union.

2. Timing gap: Between endApplyingRemoteOps() and
   startPostSyncCooldown(), isInSyncWindow() returns false, allowing
   selector-based effects to fire and create TAG:TODAY operations.
   Fix: start cooldown BEFORE ending remote ops flag.

3. No retry limit: LWW re-upload had no cap, enabling infinite loops.
   Fix: cap at 3 retries, defer remaining to next sync cycle.

Also moves MAX_VECTOR_CLOCK_SIZE and limitVectorClockSize into
@sp/shared-schema so client and server share the same pruning
constant and algorithm.
2026-01-30 18:24:29 +01:00
Johannes Millan
be4b8ba241 fix(migrations): ensure unique IDs and prevent data loss in split operations
When splitting one operation into misc and tasks operations:
- Both operations now get unique ID suffixes (_misc and _tasks)
- Skip logic verifies both conditions before skipping:
  1. tasks config has migrated field (isConfirmBeforeDelete)
  2. misc config has NO migrated fields remaining

This prevents:
- Potential operation ID conflicts in the sync log
- Data loss in partial migration scenarios

Adds test cases for partial migration and correct skip scenarios.
2026-01-20 17:07:23 +01:00
Ivan Kalashnikov
1987292433 refactor: streamline migration logic and enhance field mapping for settings 2026-01-19 14:38:42 +07:00
Ivan Kalashnikov
800ba30f4f refactor: rename misc to tasks settings migration file and object. 2026-01-19 14:30:42 +07:00
Ivan Kalashnikov
356278fc87 feat: enhance migration tests for settings and operations handling 2026-01-19 14:21:09 +07:00
Ivan Kalashnikov
218e74f882 feat: implement migration of settings from misc to tasks with operation handling 2026-01-19 14:19:31 +07:00
Ivan Kalashnikov
263495b8cd feat: update migration functions to support splitting operations into multiple results 2026-01-19 13:58:27 +07:00
Ivan Kalashnikov
b3da4e4850 fix: update CURRENT_SCHEMA_VERSION to 2 for upcoming migration 2026-01-19 13:16:48 +07:00
Ivan Kalashnikov
5002bae1c0 fix: add todo comment to bump CURRENT_SCHEMA_VERSION for upcoming migration 2026-01-19 01:09:00 +07:00
Ivan Kalashnikov
325e24f461 fix: revert CURRENT_SCHEMA_VERSION to 1 2026-01-19 00:03:33 +07:00
Ivan Kalashnikov
6705033d15 feat: implement migration to move settings from MiscConfig to TasksConfig 2026-01-18 23:31:06 +07:00
Ivan Kalashnikov
2473b9698d fix: update migration test to correctly structure migrated state with globalConfig 2026-01-18 23:30:57 +07:00
Ivan Kalashnikov
d6506e95d1 refactor: rename test spec migration in format 'v1 to v2' 2026-01-18 23:18:42 +07:00
Ivan Kalashnikov
651d5dc183 feat: add migration to move settings from MiscConfig to TasksConfig as separate file 2026-01-18 23:08:30 +07:00
Ivan Kalashnikov
f2940fd7ae fix: remove outdated todo comment regarding schema version synchronization 2026-01-18 23:07:12 +07:00
Ivan Kalashnikov
0d6d17c103 fix: correct task confirmation field name in migration test 2026-01-18 23:06:37 +07:00
Ivan Kalashnikov
5f4e1cf24e fix: correct task migration field names and add markdown formatting flag 2026-01-18 21:54:24 +07:00
Ivan Kalashnikov
73c9c05c32 refactor: rename task settings field 2026-01-18 21:53:16 +07:00
Ivan Kalashnikov
a617ff4e29 fix: update MiscConfig to mark isTurnOffMarkdown as deprecated 2026-01-18 21:50:11 +07:00
Ivan Kalashnikov
bd2615e7d7 fix: update migration versions from 16 to 1 and 17 to 2 for consistency 2026-01-18 21:28:43 +07:00
Ivan Kalashnikov
088971d22b refactor: update validateMigrationRegistry test comments for clarity 2026-01-18 21:14:22 +07:00
Ivan Kalashnikov
aedab573a0 test: add migration tests for moving settings from MiscConfig to TasksConfig 2026-01-18 21:10:05 +07:00
Ivan Kalashnikov
92d7d4aafe fix: update CURRENT_SCHEMA_VERSION to 17 for new migrations 2026-01-18 21:08:31 +07:00
Ivan Kalashnikov
b565173664 feat: add migration to move settings from MiscConfig to TasksConfig 2026-01-18 21:08:19 +07:00
Johannes Millan
23579a448a fix(shared-schema): use tsup for dual ESM/CJS build
The previous commit changed module resolution to bundler mode which
fixed Angular tests but broke Node.js ESM imports in super-sync-server.

Switch to tsup bundler that outputs both ESM (.mjs) and CJS (.js) formats,
with proper exports field to support both environments:
- CJS for super-sync-server (Node.js commonjs)
- ESM for Angular/webpack bundler imports
2026-01-11 14:05:23 +01:00
Johannes Millan
bc869d359f fix(shared-schema): fix module resolution for bundler compatibility
- Change shared-schema tsconfig to use bundler moduleResolution
- Remove .js extensions from imports (incompatible with bundler mode)
- Fix tsconfig.spec.json to include both package path mappings

The shared-schema package was using NodeNext module resolution which
requires .js extensions in imports. Since the main Angular project
uses bundler resolution and imports directly from source files,
this caused webpack to fail finding the modules during tests.
2026-01-11 13:20:16 +01:00
Johannes Millan
cbeecfd39a fix(sync): multi-tab vector clock staleness + shared code extraction
Bug fix:
- Fix vector clock cache staleness in multi-tab scenarios by clearing
  cache when acquiring operation write lock. Each browser tab has its
  own in-memory cache, so Tab B's cache could be stale if Tab A wrote
  while Tab B was waiting for the lock.

Shared code extraction (client/server consistency):
- Extract vector clock comparison to @sp/shared-schema
  - Client wraps shared impl with null handling
  - Server imports directly from shared
- Extract entity types to @sp/shared-schema
  - Single source of truth for ENTITY_TYPES array
  - Removes duplicated "must match" comments

Files:
- packages/shared-schema/src/vector-clock.ts (new)
- packages/shared-schema/src/entity-types.ts (new)
- src/app/op-log/store/operation-log-store.service.ts (cache clear)
- src/app/op-log/capture/operation-log.effects.ts (call cache clear)
2026-01-03 18:05:11 +01:00
Johannes Millan
a0965fee60 docs(sync): improve OperationLike interface documentation
Explain why OperationLike uses primitives instead of the main app's
Operation type: package isolation, portability, and stability.
2025-12-12 20:48:12 +01:00