mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Os afterChange/afterDelete de activity e googleCalendarSync rodavam a reconciliacao completa dentro da transacao do save, segurando row lock + pool por todo o I/O do Google (15s por hop, N RTTs -> 45-75s). Corrige com deadline proprio do hook (GOOGLE_CALENDAR_SYNC_HOOK_TIMEOUT_MS=5s) via AbortSignal.timeout combinado com per-hop 15s no cliente (AbortSignal.any) — manual/webhook mantem 15s sem transacao como fallback confiavel. Fail-closed preservado (abort vira paused). Pin unit hook < per-hop + int hook <=7s com fetch lento.
85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
import 'server-only'
|
||
|
||
import type { Activity, GoogleCalendarSync as GoogleCalendarSyncDoc } from '@/payload-types'
|
||
import type { PayloadRequest } from 'payload'
|
||
|
||
import {
|
||
GOOGLE_CALENDAR_SYNC_HOOK_TIMEOUT_MS,
|
||
runCampaignCalendarSync,
|
||
shouldSyncActivityOperation,
|
||
shouldSyncConfigChange,
|
||
} from '@/utilities/googleCalendarSync'
|
||
|
||
/**
|
||
* C114 — the two Payload hooks that trigger the Google mirror (in a module of
|
||
* their own so tests can mock the engine at the import boundary).
|
||
*
|
||
* Activity hook (afterChange + afterDelete): delegates the decision to the
|
||
* pure guard — task toggles, updates and result records never trigger a pass.
|
||
* afterDelete hooks carry no `operation`; its absence means delete.
|
||
* Config hook (afterChange on `googleCalendarSync`): D7 auto-resync — a
|
||
* changed `calendarId` or a re-enable runs the full reconciliation against
|
||
* the new calendar; state-only writes never re-trigger (no loop).
|
||
*
|
||
* Both NEVER throw: the Teqo write paths are independent of Google
|
||
* (fail-closed at the trigger; engine failures land in the `paused` state).
|
||
* C114-LOCK: the hooks run INSIDE the save's transaction, so the Google I/O
|
||
* is bounded by `GOOGLE_CALENDAR_SYNC_HOOK_TIMEOUT_MS` (S11 parity) — the row
|
||
* lock (`activity` / `googleCalendarSync`) is not held for the full
|
||
* `REQUEST_TIMEOUT_MS × N` RTTs. The manual / webhook paths keep the full
|
||
* per-hop budget (no transaction open) as the reliable fallback.
|
||
*/
|
||
export const activityGoogleCalendarSyncHook = async ({
|
||
doc,
|
||
previousDoc,
|
||
operation,
|
||
req,
|
||
}: {
|
||
doc: Activity
|
||
previousDoc?: Activity
|
||
operation?: 'create' | 'update'
|
||
req: PayloadRequest
|
||
}): Promise<unknown> => {
|
||
try {
|
||
// C115 — the reverse direction's own write must not re-enter the engine:
|
||
// the pass that applied the Google edit is still running; a nested pass
|
||
// would only re-list and converge (content equality) at double cost.
|
||
if (req.context?.mutationKind === 'googleCalendarSync') return doc
|
||
const resolvedOperation = operation ?? 'delete'
|
||
if (shouldSyncActivityOperation({ operation: resolvedOperation, doc, previousDoc })) {
|
||
await runCampaignCalendarSync(req.payload, {
|
||
reason: resolvedOperation,
|
||
req,
|
||
signal: AbortSignal.timeout(GOOGLE_CALENDAR_SYNC_HOOK_TIMEOUT_MS),
|
||
})
|
||
}
|
||
} catch {
|
||
// recorded as paused inside the engine; the write path stays intact
|
||
}
|
||
return doc
|
||
}
|
||
|
||
export const googleCalendarSyncConfigHook = async ({
|
||
doc,
|
||
previousDoc,
|
||
operation,
|
||
req,
|
||
}: {
|
||
doc: GoogleCalendarSyncDoc
|
||
previousDoc?: GoogleCalendarSyncDoc
|
||
operation: 'create' | 'update'
|
||
req: PayloadRequest
|
||
}): Promise<unknown> => {
|
||
try {
|
||
if (shouldSyncConfigChange({ operation, doc, previousDoc })) {
|
||
await runCampaignCalendarSync(req.payload, {
|
||
reason: 'config-change',
|
||
req,
|
||
signal: AbortSignal.timeout(GOOGLE_CALENDAR_SYNC_HOOK_TIMEOUT_MS),
|
||
})
|
||
}
|
||
} catch {
|
||
// never throw into admin writes
|
||
}
|
||
return doc
|
||
}
|