( ctx: PollWebhookContext, config: HubSpotWebhookConfig, accessToken: string )
| 348 | } |
| 349 | |
| 350 | async function pollListMembership( |
| 351 | ctx: PollWebhookContext, |
| 352 | config: HubSpotWebhookConfig, |
| 353 | accessToken: string |
| 354 | ): Promise<'success' | 'failure'> { |
| 355 | const { webhookData, workflowData, requestId, logger } = ctx |
| 356 | const webhookId = webhookData.id |
| 357 | |
| 358 | const listId = config.listId?.trim() |
| 359 | if (!listId) { |
| 360 | throw new Error(`HubSpot list_membership trigger ${webhookId} is missing listId`) |
| 361 | } |
| 362 | const nowMs = Date.now() |
| 363 | const seedComplete = config.membershipSeedComplete === true |
| 364 | const maxRecords = Math.min( |
| 365 | Math.max(config.maxRecordsPerPoll ?? DEFAULT_MAX_RECORDS, 1), |
| 366 | MAX_MAX_RECORDS |
| 367 | ) |
| 368 | |
| 369 | // The HubSpot endpoint walks ASC by default. We resume from a stored `after` cursor — |
| 370 | // empty cursor means "from the beginning of the list". During the seed pass we paginate |
| 371 | // forward without emitting; once we reach the end (no `paging.next.after`) we mark the |
| 372 | // seed complete and re-fetch from the cursor-to-last-page on each normal poll. New |
| 373 | // members appended to the list show up on subsequent fetches; the idempotency layer |
| 374 | // dedups the records we've already seen on the boundary page. |
| 375 | const result = await fetchListMembershipPages({ |
| 376 | listId, |
| 377 | accessToken, |
| 378 | initialAfter: config.lastSeenMembershipCursor?.trim() || undefined, |
| 379 | pageLimit: seedComplete ? maxRecords : MAX_PAGES_PER_POLL * HUBSPOT_PAGE_LIMIT, |
| 380 | requestId, |
| 381 | logger, |
| 382 | }) |
| 383 | |
| 384 | if (!seedComplete) { |
| 385 | // Seed phase: don't emit. Just save the cursor and (when reached) flip the flag. |
| 386 | const update: Record<string, unknown> = { |
| 387 | lastCheckedTimestamp: new Date(nowMs).toISOString(), |
| 388 | lastSeenMembershipCursor: result.resumeCursor ?? '', |
| 389 | } |
| 390 | if (result.reachedEnd) { |
| 391 | update.membershipSeedComplete = true |
| 392 | logger.info( |
| 393 | `[${requestId}] HubSpot list_membership ${webhookId} seed complete (list=${listId})` |
| 394 | ) |
| 395 | } else { |
| 396 | logger.info( |
| 397 | `[${requestId}] HubSpot list_membership ${webhookId} seed in progress (list=${listId}, scanned ${result.scanned})` |
| 398 | ) |
| 399 | } |
| 400 | await updateWebhookProviderConfig(webhookId, update, logger) |
| 401 | await markWebhookSuccess(webhookId, logger) |
| 402 | return 'success' |
| 403 | } |
| 404 | |
| 405 | if (result.records.length === 0) { |
| 406 | await updateWebhookProviderConfig( |
| 407 | webhookId, |
no test coverage detected