Skip to content

refactor(calendar): generalize the sync engine off Google (#1393) - #1423

Draft
h4yfans wants to merge 1 commit into
h4yfans/calendar-provider-registryfrom
h4yfans/calendar-sync-engine-generalization
Draft

refactor(calendar): generalize the sync engine off Google (#1393)#1423
h4yfans wants to merge 1 commit into
h4yfans/calendar-provider-registryfrom
h4yfans/calendar-sync-engine-generalization

Conversation

@h4yfans

@h4yfans h4yfans commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #1393. Phase 0 of the multi-provider calendar epic #1390.

Stacked on #1422 (#1392), which is stacked on #1407 (#1391). Base is h4yfans/calendar-provider-registry.

What

google/sync-service.ts held the whole engine in 917 lines. What bound it to Google was narrow and countable: a dozen provider: 'google' literals, a direct createGoogleCalendarClient() import, and a module-global syncInFlight boolean — one slot, shared by everything.

The engine moves to calendar/sync/ and takes a ProviderSyncContext (adapter factory, account routing, connection check, push toggle, default target calendar):

Was Now
discoverGoogleCalendarSources discoverProviderSources
syncGoogleCalendarNow syncProviderNow
pushSourceToGoogleCalendar pushSourceToProvider
deleteSourceFromGoogleCalendar deleteSourceFromProvider
applyGoogleCalendarWriteback applyProviderWriteback
applyGoogleCalendarDelete applyProviderDelete
syncGoogleCalendarSource syncProviderSource

Behavior rules, now enforced by the engine

  • Writes are gated by capabilities.supportsWrite at the engine level, not left to adapter politeness. A read-only provider cannot reach calendar_bindings through push, through the local-change path, or through inbound sync — and it never provisions a memrynote calendar.
  • incrementalMode drives cursor handling. Only sync-token / delta-link treat a missing cursor as invalidation. A conditional-get feed has no cursor to lose, so the reset branch cannot fire for it — otherwise every ICS poll would re-read from scratch forever.
  • ProviderGoneError clears sync_cursor and re-runs the source. Google's plain status: 410 shape is still recognized, so today's behavior is bit-for-bit unchanged.
  • syncInFlight is a per-provider set. One slow provider no longer blocks every other one.
  • The runner is per-provider: its own timer, resume listener, trigger cooldown and cadence. PUSH_BACKOFF_INTERVAL_MS still applies to push-capable providers; supportsPush: false stays on the polling cadence. FOCUS_TRIGGER_COOLDOWN_MS and WINDOW_FOCUS_REASON behavior is preserved exactly.
  • ProviderRateLimitError parks that provider until its own retryAfterMs elapses, without touching any other provider's schedule.
  • push-conflict-retry.ts is kept and expresses the 412 case as ProviderConflictError + If-Match; Google's raw status: 412 still counts.

How the Google suite stayed untouched

Rather than rewrite the Google tests, Google is expressed as one bound context in providers/google/sync-service.ts that keeps every exported name and signature. google-sync-runner.ts does the same over the generic runner. mappers.ts moved to sync/remote-event-mappers.ts — it was always provider-neutral, it maps the shared remote-event shape — with a re-export left at the old path so mappers.test.ts did not even move.

Result: the entire existing Google calendar suite passes with zero edits — no assertion, no mock, no import path. That is the correctness proof the issue asked for.

engine.ts crossed the 800-line max-lines limit, so the inbound half (applyProviderWriteback / applyProviderDelete and their publishers) is split into sync/writeback.ts, the same way push-conflict-retry.ts was split out originally.

Verification

  • pnpm typecheck — 16/16 green
  • pnpm lint — 0 errors
  • Desktop, per project: shared 2455 · main 6363 passed / 1 expected fail / 4 skipped (517 files) · main-integration 9 · renderer 6965 passed / 6 skipped

New sync/engine.test.ts (9 tests) and sync/runner.test.ts (6 tests) against an in-memory fake adapter, covering exactly the issue's checklist:

  • two providers sync concurrently, neither blocks the other — and a second sweep of the same provider is still refused
  • a supportsWrite: false adapter produces no calendar_bindings row under any path (direct push, local-change path, inbound sync), and no memrynote calendar
  • ProviderGoneError resets the cursor and re-runs with syncCursor: null
  • a missing cursor resets a sync-token provider but is normal for conditional-get
  • ProviderRateLimitError makes the runner wait retryAfterMs, skipping periodic polls inside the window and resuming after it, without throttling any other provider
  • one timer per provider; stopping one leaves the other running

One pre-existing test changed: inbox/sync-enqueue-guard.test.ts is a tripwire listing every module that writes inbox triage state, and the inbox_snooze writeback moved from calendar/providers/google/sync-service.ts to calendar/sync/writeback.ts. The guard caught the move — exactly its job — so its expected path was updated. No assertion logic changed.

Docs

MEMRY_DOCS_IMPACT_SKIP=1 on push: no user-facing change. Google is still the only provider, the cadence and cooldowns are identical, and no setting, schema or channel moved. Docs land with #1394 and #1396.

@github-actions github-actions Bot added the test label Aug 13, 2026
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit ec745e7.

`google/sync-service.ts` held the whole engine — source discovery, push,
writeback, delete, per-source sync — in 917 lines, bound to Google by a dozen
`provider: 'google'` literals, a direct `createGoogleCalendarClient()` import,
and a module-global `syncInFlight` boolean shared by everything.

The engine moves to `calendar/sync/` and is parameterized on a
`ProviderSyncContext` — adapter factory, account routing, connection check,
push toggle, default target calendar:

| Was | Now |
|---|---|
| `discoverGoogleCalendarSources` | `discoverProviderSources` |
| `syncGoogleCalendarNow` | `syncProviderNow` |
| `pushSourceToGoogleCalendar` | `pushSourceToProvider` |
| `deleteSourceFromGoogleCalendar` | `deleteSourceFromProvider` |
| `applyGoogleCalendarWriteback` | `applyProviderWriteback` |
| `applyGoogleCalendarDelete` | `applyProviderDelete` |
| `syncGoogleCalendarSource` | `syncProviderSource` |

Behavior rules the engine now enforces:

- Writes are gated by `capabilities.supportsWrite` at the engine, not left to
  adapter politeness. A read-only provider cannot reach `calendar_bindings`
  through push, through the local-change path, or through inbound sync, and it
  never provisions a memrynote calendar.
- `incrementalMode` drives cursor handling. Only `sync-token` / `delta-link`
  treat a missing cursor as invalidation — a `conditional-get` feed has no
  cursor to lose, so the reset branch cannot fire for it.
- `ProviderGoneError` clears `sync_cursor` and re-runs the source. Google's
  plain `status: 410` shape is still recognized, so its behavior is unchanged.
- `syncInFlight` is a per-provider set. One slow provider no longer blocks
  every other one.
- The runner is per-provider: its own timer, resume listener, cooldown and
  cadence. `PUSH_BACKOFF_INTERVAL_MS` still applies to push-capable providers;
  `supportsPush: false` stays on the polling cadence.
  `FOCUS_TRIGGER_COOLDOWN_MS` and `WINDOW_FOCUS_REASON` behavior is preserved.
- `ProviderRateLimitError` parks that provider until its own `retryAfterMs`
  elapses, without touching any other provider's schedule.
- `push-conflict-retry.ts` is kept and now expresses the 412 case as
  `ProviderConflictError` + `If-Match`; Google's raw `status: 412` still counts.

Google becomes one bound context in `providers/google/sync-service.ts`, keeping
every exported name so no caller changes. `mappers.ts` moved to
`sync/remote-event-mappers.ts` (it was always neutral — it maps the shared
remote-event shape) with a re-export left behind, so its test never moved.

The entire existing Google calendar suite passes with zero edits.
@h4yfans
h4yfans force-pushed the h4yfans/calendar-provider-registry branch from 36bcac9 to f80627b Compare August 13, 2026 16:17
@h4yfans
h4yfans force-pushed the h4yfans/calendar-sync-engine-generalization branch from 302f054 to ec745e7 Compare August 13, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant