Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
148 lines
7.1 KiB
Markdown
148 lines
7.1 KiB
Markdown
# Gmail Pub/Sub Initialisation – Single-Run Refactor Plan
|
||
|
||
## High-level Goal
|
||
|
||
Prevent multiple initialisations of the Gmail Pub/Sub topic & subscription that currently occur during OAuth callback **and** on every provider save. Each logical trigger (OAuth callback or an explicit “Refresh Pub/Sub” action) must result in **exactly one** call to `setupPubSub`, ensuring Google Cloud does not disable the subscription for excessive creation attempts.
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. Background & Current Problem
|
||
2. Target Architecture (diagram & responsibilities)
|
||
3. Phased To-Do List
|
||
4. Acceptance Criteria / Testing
|
||
5. Roll-out Strategy
|
||
|
||
---
|
||
|
||
## 1. Background & Current Problem
|
||
|
||
Action flow today when a user connects Gmail via OAuth:
|
||
|
||
| Step | Stack Trace | Calls to `setupPubSub` |
|
||
|------|------------|-------------------------|
|
||
| 1 | `upsertEmailProvider → finalizeGoogleProvider` | ① |
|
||
| 2 | `finalizeGoogleProvider → initializeProviderWebhook → GmailWebhookService.setupGmailWebhook` | ② |
|
||
| 3 | UI `onSubmit` (provider **Save**) → `updateEmailProvider → finalizeGoogleProvider` | ③ |
|
||
| 4 | Same nested path as step 2 | ④ |
|
||
|
||
Result : **four** attempts to create/modify the exact same topic & subscription – Google rejects or eventually stops the subscription.
|
||
|
||
Root cause is split responsibility: both *orchestration* (`finalizeGoogleProvider`) and *Gmail layer* (`GmailWebhookService`) attempt to create Pub/Sub resources.
|
||
|
||
---
|
||
|
||
## 2. Target Architecture
|
||
|
||
```
|
||
OAuth Callback ─┬─ store tokens
|
||
└─ configureGmailProvider() ← the *only* place that calls setupPubSub
|
||
|
||
configureGmailProvider()
|
||
├─ generatePubSubNames()
|
||
├─ setupPubSub() ← single invocation per logical trigger
|
||
└─ GmailWebhookService.registerWatch() (watch only)
|
||
|
||
UI Save (settings form)
|
||
└─ upsert / update provider (skipAutomation=true) ← does *not* touch Pub/Sub
|
||
|
||
Admin “Refresh Pub/Sub”
|
||
└─ configureGmailProvider() (force=true)
|
||
```
|
||
|
||
Responsibilities
|
||
|
||
* **configureGmailProvider** – orchestration; knows tenant / topic naming; idempotent.
|
||
* **GmailWebhookService** – Gmail-specific; *never* deals with Pub/Sub.
|
||
* **setupPubSub** – low-level GCP logic, unchanged.
|
||
|
||
Database adds `google_config.pubsub_initialised_at` timestamp for an extra idempotency guard.
|
||
|
||
---
|
||
|
||
## 3. Implementation Checklist
|
||
|
||
### Phase 1: Foundation & Database Schema
|
||
- [x] Create database migration to add `pubsub_initialised_at TIMESTAMPTZ` field to `google_email_provider_config` table
|
||
- File: `server/migrations/20250719133450_add_pubsub_initialised_at_to_google_config.cjs`
|
||
- [x] Update `GoogleEmailProviderConfig` interface to include `pubsub_initialised_at?: string`
|
||
- File: `server/src/components/EmailProviderConfiguration.tsx:73`
|
||
- [x] Create `server/src/lib/actions/email-actions/configureGmailProvider.ts`
|
||
- Copy logic from `finalizeGoogleProvider` (`emailProviderActions.ts:280-337`)
|
||
- Add `force?: boolean` parameter for admin refresh actions
|
||
- Implement idempotency: return early if `pubsub_initialised_at` < 24h old and `!force`
|
||
- Call `setupPubSub()` with existing interface
|
||
- Call new `GmailWebhookService.registerWatch()` (not `setupGmailWebhook`)
|
||
- Update `pubsub_initialised_at = NOW()` after successful Pub/Sub setup
|
||
- [x] Export `configureGmailProvider` from email-actions index barrel if present (no index barrel exists)
|
||
|
||
### Phase 2: Trim GmailWebhookService
|
||
- [x] Remove setupPubSub import and calls from `GmailWebhookService.ts:41-84`
|
||
- Delete `import { setupPubSub }` and related imports
|
||
- Remove Step 1 ("Set up Pub/Sub") from method
|
||
- [x] Rename `setupGmailWebhook()` → `registerWatch()` in `GmailWebhookService.ts`
|
||
- Method now only registers Gmail watch via `GmailAdapter.registerWebhookSubscription()`
|
||
- [x] Update return type `GmailWebhookSetupResult` to remove `topicPath` and `subscriptionPath` fields
|
||
- Renamed to `GmailWatchRegistrationResult` and removed topic/subscription fields
|
||
- [x] Update all call sites that reference `setupGmailWebhook` to use `registerWatch`
|
||
|
||
### Phase 3: Wire Up New Orchestrator
|
||
- [x] Replace `finalizeGoogleProvider` calls with `configureGmailProvider`:
|
||
- In `upsertEmailProvider()` (`emailProviderActions.ts:419-454`)
|
||
- In `updateEmailProvider()` (`emailProviderActions.ts:469-507`)
|
||
- [x] Remove or reduce `finalizeGoogleProvider` to thin wrapper (or delete entirely)
|
||
|
||
### Phase 4: UI & Automation Control
|
||
- [x] Add `skipAutomation?: boolean = false` parameter to:
|
||
- `upsertEmailProvider()` signature and implementation
|
||
- `updateEmailProvider()` signature and implementation
|
||
- `createEmailProvider()` signature and implementation
|
||
- [x] Only call `configureGmailProvider()` when `!skipAutomation`
|
||
- [x] Update UI provider form `onSubmit()` in `GmailProviderForm.tsx:91-152`:
|
||
- Modify to pass `skipAutomation: true` for normal saves
|
||
- Keep OAuth flow with `skipAutomation: false` (default)
|
||
- [x] Add "Refresh Pub/Sub" button that calls `configureGmailProvider()` with `force: true`
|
||
- Updated existing "Refresh Watch Subscription" dropdown to use `configureGmailProvider(force: true)`
|
||
- Modified `/api/email/refresh-watch` endpoint to call new orchestrator
|
||
- Updated UI text to "Refresh Pub/Sub & Watch" to reflect complete refresh
|
||
|
||
## Current Architecture Analysis
|
||
|
||
**Files Involved:**
|
||
- `server/src/lib/actions/email-actions/emailProviderActions.ts` - Main provider CRUD, contains `finalizeGoogleProvider`
|
||
- `server/src/services/email/GmailWebhookService.ts` - Gmail webhook service, currently calls `setupPubSub`
|
||
- `server/src/services/email/EmailProviderService.ts` - Contains `initializeProviderWebhook`
|
||
- `server/src/lib/actions/email-actions/setupPubSub.ts` - Low-level Pub/Sub setup
|
||
- `server/src/components/GmailProviderForm.tsx` - UI form for Gmail provider settings
|
||
|
||
**Current Problem Flow:**
|
||
1. OAuth: `upsertEmailProvider()` → `finalizeGoogleProvider()` → `setupPubSub()` ①
|
||
2. Webhook: `finalizeGoogleProvider()` → `EmailProviderService.initializeProviderWebhook()` → `GmailWebhookService.setupGmailWebhook()` → `setupPubSub()` ②
|
||
3. UI Save: `updateEmailProvider()` → `finalizeGoogleProvider()` → `setupPubSub()` ③
|
||
4. Nested: Same as step 2 ④
|
||
|
||
**Target Flow:**
|
||
- OAuth/Admin: `configureGmailProvider()` → `setupPubSub()` (once) + `registerWatch()`
|
||
- UI Save: `updateEmailProvider(skipAutomation: true)` → (no Pub/Sub calls)
|
||
|
||
|
||
## 4. Roll-out Strategy
|
||
|
||
1. **Stage 1 – Dual Behaviour**
|
||
Backend supports `skipAutomation` but default is `false` (old behaviour).
|
||
Ship code; ensures no breakage if UI lags behind.
|
||
|
||
2. **Stage 2 – Frontend Update**
|
||
Release UI with `skipAutomation: true` on normal save and new “Refresh” button.
|
||
|
||
3. **Stage 3 – Final Toggle**
|
||
Change backend default of `skipAutomation` to `true` and delete old `finalizeGoogleProvider` dead code.
|
||
|
||
4. Monitor logs for unexpected additional Pub/Sub calls; once stable for a week, close ticket.
|
||
|
||
---
|
||
|
||
> **Outcome** – Every logical trigger performs **exactly one** Pub/Sub setup. Google Cloud stays happy; our subscriptions remain active; and the codebase gains a clear separation of concerns that is easy for new contributors to understand.
|
||
|