* Walks `/lists/{listId}/memberships/join-order` ASC from an optional cursor until either * (a) the per-poll page cap is reached or (b) the API stops returning a next cursor. * * The endpoint returns members in ascending join-order by default. We never use `before` * (DESC mode) because its boot
( args: FetchListMembershipPagesArgs )
| 898 | * they're appended past our cursor's position. |
| 899 | */ |
| 900 | async function fetchListMembershipPages( |
| 901 | args: FetchListMembershipPagesArgs |
| 902 | ): Promise<FetchListMembershipPagesResult> { |
| 903 | const { listId, accessToken, initialAfter, pageLimit, requestId, logger } = args |
| 904 | |
| 905 | const records: ListMembershipRecord[] = [] |
| 906 | let after: string | undefined = initialAfter |
| 907 | let pages = 0 |
| 908 | let reachedEnd = false |
| 909 | let scanned = 0 |
| 910 | |
| 911 | while (pages < MAX_PAGES_PER_POLL) { |
| 912 | const params = new URLSearchParams({ limit: String(HUBSPOT_PAGE_LIMIT) }) |
| 913 | if (after) params.set('after', after) |
| 914 | const url = `https://api.hubapi.com/crm/v3/lists/${encodeURIComponent(listId)}/memberships/join-order?${params.toString()}` |
| 915 | const response = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } }) |
| 916 | |
| 917 | if (!response.ok) { |
| 918 | const errorText = await response.text().catch(() => '') |
| 919 | logger.error( |
| 920 | `[${requestId}] HubSpot list memberships fetch failed ${response.status}: ${errorText}` |
| 921 | ) |
| 922 | throw new Error( |
| 923 | `HubSpot list memberships fetch ${response.status}: ${errorText.slice(0, 500)}` |
| 924 | ) |
| 925 | } |
| 926 | |
| 927 | const data = (await response.json()) as { |
| 928 | results?: Array<{ recordId: string; membershipTimestamp: string }> |
| 929 | paging?: { next?: { after?: string } } |
| 930 | } |
| 931 | const batch = data.results ?? [] |
| 932 | scanned += batch.length |
| 933 | const nextAfter = data.paging?.next?.after |
| 934 | |
| 935 | for (const m of batch) { |
| 936 | records.push({ |
| 937 | recordId: m.recordId, |
| 938 | membershipTimestamp: m.membershipTimestamp, |
| 939 | }) |
| 940 | } |
| 941 | |
| 942 | pages++ |
| 943 | if (!nextAfter) { |
| 944 | reachedEnd = true |
| 945 | break |
| 946 | } |
| 947 | after = nextAfter |
| 948 | if (records.length >= pageLimit) break |
| 949 | } |
| 950 | |
| 951 | return { |
| 952 | records, |
| 953 | // If we walked to the end, hold onto the cursor we LAST used so the next poll re-fetches |
| 954 | // the tail page and picks up any members appended since. If we stopped early, the next |
| 955 | // cursor walks us forward through the rest of the list. |
| 956 | resumeCursor: after, |
| 957 | reachedEnd, |