Back to Super Productivity

Sync Package Boundaries

docs/sync-and-op-log/package-boundaries.md

18.6.07.1 KB
Original Source

Sync Package Boundaries

Status: Active Last Updated: May 13, 2026

This note documents the package split used by the operation-log sync stack. The goal is to keep reusable sync logic framework-agnostic while leaving Super Productivity domain wiring in the app.

Dependency Direction

Allowed direction:

text
src/app
  -> @sp/sync-providers
  -> @sp/sync-core

src/app
  -> @sp/sync-core

packages/shared-schema
  -> @sp/sync-core

Rules:

  • @sp/sync-core must not import Angular, NgRx, src/app, @sp/shared-schema, @sp/sync-providers, or provider-specific code.
  • @sp/sync-providers may import only public @sp/sync-core exports. It must not deep-import @sp/sync-core/*, Angular, NgRx, src/app, or @sp/shared-schema.
  • The app may import both packages and is responsible for Angular dependency injection, NgRx, Electron/Capacitor bridges, config UI, OAuth routing, and Super Productivity-specific model wiring.
  • packages/shared-schema depends on @sp/sync-core only for compatibility re-exports of generic vector-clock algorithms.

The packages/shared-schema -> @sp/sync-core edge is deliberate compatibility coupling. Vector-clock compare/merge/prune algorithms moved to @sp/sync-core so sync-core, client wrappers, and server/shared consumers use one implementation. packages/shared-schema re-exports those algorithms to preserve legacy import paths while consumers migrate; do not add new sync-engine logic to @sp/shared-schema, and remove the compatibility edge once no consumers need it.

Ownership

@sp/sync-core owns reusable sync-engine primitives:

  • generic operation and apply types;
  • vector-clock compare, merge, and prune algorithms;
  • pure conflict, import-filter, upload/download, replay, compression, and prefix helpers;
  • structural entity-registry contracts;
  • app-facing port contracts and the privacy-aware SyncLogger interface.

@sp/sync-providers owns bundled provider implementations and provider-neutral contracts:

  • Dropbox, WebDAV, Nextcloud, SuperSync, and LocalFile provider classes;
  • file-based sync envelope types and provider response contracts;
  • provider-owned file envelope constants such as sync-data.json and file-sync version keys;
  • credential, file-adapter, platform-info, web-fetch, native-HTTP, storage, and response-validator ports;
  • provider-shared error classes, PKCE helpers, retry helpers, and safe logging metadata helpers.

Cross-provider utilities belong in @sp/sync-providers when they are reusable by provider implementations but not by the generic engine. Existing examples are provider-shared error classes, PKCE, retry predicates, native-HTTP retry, and safe log-metadata helpers.

New bundled providers should follow the Dropbox/WebDAV/SuperSync/LocalFile pattern: put provider-owned protocol logic and provider-neutral contracts in @sp/sync-providers, then compose app-only credentials, platform bridges, validators, OAuth routing, and UI config in thin app-side factories. If a provider is app-specific or plugin-provided rather than bundled, implement it app-side against the provider contracts instead of widening the package surface.

src/app owns host-specific configuration and choreography:

  • ActionType, ENTITY_TYPES, SyncProviderId, provider lists, and storage prefixes such as REMOTE_FILE_CONTENT_PREFIX and PRIVATE_CFG_PREFIX;
  • entity registry construction from feature reducers/selectors;
  • wrapped full-state payload shape, import reasons, repair payloads, and validation against @sp/shared-schema;
  • Angular services, NgRx dispatch/replay conversion, local-action filtering, hydration windows, archive side effects, provider factories, OAuth callbacks, config dialogs, and platform bridge implementations.

packages/shared-schema owns Super Productivity schema contracts and validators that are shared between app and server. In this boundary it should stay SP-coupled and should not become a dependency of @sp/sync-core or @sp/sync-providers.

Public Exports

Package consumers should import from package barrels only:

ts
import { compareVectorClocks } from '@sp/sync-core';
import { Dropbox, PROVIDER_ID_DROPBOX } from '@sp/sync-providers/dropbox';

Do not import from package internals such as @sp/sync-core/src/*, @sp/sync-providers/src/*, or dist/*. If a host needs a symbol, promote it to the package barrel deliberately and check that it is not app-owned.

The root @sp/sync-providers barrel has been removed; consumers MUST import from focused subpath barrels: @sp/sync-providers/dropbox, /webdav, /super-sync, /local-file, /http, /errors, /file-based, /pkce, /platform, /provider-types, /credential-store, and /log. Provider classes, provider-owned string constants, and shared privacy-boundary logging helpers are exported there, but app enums such as SyncProviderId are not. Internal helpers such as WebDAV API/adapter classes stay unexported unless a second host needs them.

@sp/sync-core still exports deprecated full-state op compatibility defaults and host-defined OpType.SyncImport / BackupImport / Repair strings for existing consumers. New reusable hosts should provide their own full-state operation strings through createFullStateOpTypeHelpers().

Privacy Boundary

Package logging must use SyncLogger and safe structured metadata only. SyncLogger is a privacy-aware port shape; it does not sanitize arbitrary metadata. Call sites are responsible for passing already-scrubbed values, and enforcement is currently code review plus focused tests.

IDs, counts, action strings, entity types, provider IDs, and error names/codes are acceptable. URL metadata is acceptable only after the caller strips query strings, fragments, credentials, tokens, raw response bodies, and user-provided path segments such as file names, emails, share IDs, or folder names. Prefer coarse path templates, provider operation names, host-only values, or a provider-owned relative path category over raw URL paths.

Full entities, operation payloads, task titles, note text, raw provider responses, credentials, headers, and encryption material must stay out of exportable logs. A lint rule for unsafe direct logging remains a possible follow-up; until then, new movable/provider code should use SyncLogger and tests should assert privacy-sensitive catch paths.

Tests

The ESLint package-boundary overrides apply to all TypeScript files under packages/sync-core/** and packages/sync-providers/**, including tests. Tests may import their own package internals through relative paths for white-box coverage. @sp/sync-providers tests may import public @sp/sync-core exports, but should not import @sp/sync-core internals or sync-core test helpers.

Verification

Before moving code across these boundaries, run:

bash
npm run lint
npm run sync-core:build
npm run sync-providers:build
npm run packages:test

For a quick boundary spot-check, use:

bash
rg -n "from ['\"](@angular|@ngrx|@sp/shared-schema|src/app|@sp/sync-core/src|@sp/sync-core/)|import\(['\"](@angular|@ngrx|@sp/shared-schema|src/app|@sp/sync-core/src|@sp/sync-core/)" packages/sync-core/src packages/sync-providers/src