Skip to content

refactor(calendar): provider-neutral adapter interface and error taxonomy (#1391) - #1407

Draft
h4yfans wants to merge 2 commits into
mainfrom
h4yfans/calendar-provider-adapter-interface
Draft

refactor(calendar): provider-neutral adapter interface and error taxonomy (#1391)#1407
h4yfans wants to merge 2 commits into
mainfrom
h4yfans/calendar-provider-adapter-interface

Conversation

@h4yfans

@h4yfans h4yfans commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Closes #1391. Phase 0 of the multi-provider calendar epic #1390. Bottom of the stack.

What

The calendar layer already had an abstraction — GoogleCalendarClient — but Google concepts leaked through it: watchCalendar/stopChannel are literally Google's push-channel API, syncCursor carried syncToken semantics, and there was no error taxonomy at all (sync-service.ts sniffed status === 410 off Google's own wire shape).

New apps/desktop/src/main/calendar/provider/:

  • adapter.tsCalendarProviderAdapter plus ProviderCapabilities: supportsWrite, supportsCreateCalendar, supportsPush, supportsMultiAccount, incrementalMode (sync-token | delta-link | sync-collection | ctag-etag | conditional-get | full), authFlow (oauth2 | basic | url | none). Optional adapter members map one-to-one onto the capability flags, so a read-only provider simply has no upsertEvent.
  • errors.tsProviderAuthError (drives reconnect_required), ProviderGoneError (cursor invalid → full resync), ProviderConflictError (412 / etag), ProviderRateLimitError (carries retryAfterMs), ProviderTransientError. Adapters translate their own wire errors into these so the engine reacts to the condition rather than to one vendor's status code.

Renames, field names preserved verbatim (raw included):

Was Now
GoogleCalendarRemoteEvent RemoteCalendarEvent
GoogleCalendarUpsertEventInput UpsertRemoteEventInput
GoogleCalendarDescriptor RemoteCalendarDescriptor

The Google-era names stay as aliases in calendar/types.ts for the existing Google call sites.

main/calendar/google/main/calendar/providers/google/. The Google client implements the neutral interface: watch/unwatch are the names the engine sees, and watchCalendar/stopChannel are kept as the Google-named pair the push-channel manager and the sync-server relay still speak (generalized later, in #1404).

Backward compatibility

No DB schema change, no IPC contract change, no settings shape change, no wire-format change. This is a type-level and file-layout refactor only — git diff --stat is 58 files, almost all of it path bumps.

Verification

  • pnpm --filter @memry/desktop typecheck (node + web + test) — green
  • pnpm lint — 0 errors
  • pnpm check:architecture / pnpm check:contracts — green
  • Desktop suite, per project (the all-projects run SIGSEGVs on this machine, a known vitest/turbo flake):
    • shared — 142 files, 2455 tests passed
    • main — 512 files, 6330 passed / 1 expected fail / 4 skipped
    • main-integration — 1 file, 9 passed
    • renderer — 609 files, 6965 passed / 6 skipped
  • The existing Google tests (sync-service.test.ts 2374 lines, client.test.ts 841, oauth.test.ts 840, mappers.test.ts, push-runtime.test.ts, google-channel-manager.test.ts) moved with the directory and were changed only by the relative-import depth bump. No assertion, mock or fixture was edited.
  • New: provider/errors.test.ts (5 tests) covering the taxonomy.

The four calendar/google/*.test.ts entries in the tsconfig.test.node.json exclude backlog had their paths updated in place. I checked whether they could be dropped instead — they still fail on the pre-existing TestDb vs DataDb mismatch that put them there, which is unrelated to this change, so the backlog kept the same four entries and did not grow.

Docs

MEMRY_DOCS_IMPACT_SKIP=1 on push: nothing user-facing changed. No new setting, no new surface, no behavior difference — every touched file is an import path or an internal type. Docs land with #1394 (agent read-consent policy) and #1396 (cross-version compat findings).

Also in this PR

fix(scripts): stop the secret scanner flagging an async arrow over a call — the directory rename restages push-runtime.ts, and CI's Secret scan flagged

hashToken: async (plaintext) => createHmac('sha256', hmacKey)…

as a high-risk-secret-assignment. hashToken matches the TOKEN keyword, and the existing arrow-function exemption rejects any quote — deliberately, so an embedded literal stays flagged — which also rejects the string arguments of an ordinary call. A callee name followed by ( is the discriminator: a bare literal body like () => 'sk-live-…' has no callee and is still flagged. Both cases are covered by new tests in scripts/check-staged-secrets.test.mjs (19/19 green).

It lives at the bottom of the stack because that is where the rename happens.

…nomy (#1391)

The calendar layer already had an abstraction — `GoogleCalendarClient` — but
Google concepts had leaked through it: `watchCalendar`/`stopChannel` are
literally Google's push-channel API, `syncCursor` carried syncToken semantics,
and error handling was a `status === 410` sniff on Google's own wire shape.

Introduce `calendar/provider/`:

- `adapter.ts` — `CalendarProviderAdapter` plus `ProviderCapabilities`
  (`supportsWrite`, `supportsCreateCalendar`, `supportsPush`,
  `supportsMultiAccount`, `incrementalMode`, `authFlow`). Optional adapter
  members map one-to-one onto the capability flags. `RemoteCalendarEvent` /
  `UpsertRemoteEventInput` / `RemoteCalendarDescriptor` carry the Google-era
  field names verbatim, `raw` included, so the mappers and the
  `calendar_external_events` mirror are untouched.
- `errors.ts` — `ProviderAuthError`, `ProviderGoneError`,
  `ProviderConflictError`, `ProviderRateLimitError` (with `retryAfterMs`),
  `ProviderTransientError`. Adapters translate their own wire errors into
  these so the engine reacts to the condition, not to one vendor's status code.

`main/calendar/google/` moves to `main/calendar/providers/google/`. The Google
client now implements the neutral interface; `watch`/`unwatch` are the names
the engine sees, with `watchCalendar`/`stopChannel` kept as the Google-named
pair the push-channel manager and the sync-server relay still speak.

No behavior change: no DB schema change, no IPC contract change, no settings
shape change. The existing Google tests move with the directory and are
otherwise untouched — that is the correctness proof for the refactor.
@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 bdad38b.

@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…call

Renaming `calendar/google/` to `calendar/providers/google/` restages
`push-runtime.ts`, and the scanner flagged

    hashToken: async (plaintext) => createHmac('sha256', hmacKey)…

as a `high-risk-secret-assignment`. `hashToken` matches the TOKEN keyword, and
the existing arrow-function exemption rejects any quote — deliberately, so an
embedded literal stays flagged — which also rejects the string arguments of an
ordinary call.

A callee name followed by `(` is the discriminator: a bare literal body like
`() => 'sk-live-…'` has no callee and is still flagged. Both cases are covered
by new tests.
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.

[calendar][P0] Provider adapter interface + capability model

1 participant