Skip to content

Commit 324741d

Browse files
committed
fix(hubspot): Map-backed property snapshot + drop redundant filter parse
1 parent 4c27a1f commit 324741d

1 file changed

Lines changed: 46 additions & 28 deletions

File tree

apps/sim/lib/webhooks/polling/hubspot.ts

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,29 @@ interface HubSpotWebhookConfig {
5757
* workflow doesn't see a flood of historical members on activation.
5858
*/
5959
membershipSeedComplete?: boolean
60-
/** Snapshot of the watched property's last-seen value per record (property_changed event). */
60+
/**
61+
* Snapshot of the watched property's last-seen value per record (property_changed event).
62+
* Persisted as an entries array (not a `Record`) because HubSpot record ids are numeric
63+
* strings, and JS engines enumerate integer-indexed object keys in numeric order
64+
* regardless of insertion — which would break LRU-style trimming. Array order is stable.
65+
*/
6166
propertySnapshot?: {
6267
property: string
63-
values: Record<string, string | null>
68+
/** `[recordId, value]` pairs in LRU order — oldest first. */
69+
entries: Array<[string, string | null]>
6470
}
6571
lastCheckedTimestamp?: string
6672
}
6773

74+
/**
75+
* In-memory snapshot state used during a single poll. Uses a Map (not a plain object) so
76+
* insertion-order iteration is honored even for numeric-string keys.
77+
*/
78+
interface PropertySnapshotState {
79+
property: string
80+
values: Map<string, string | null>
81+
}
82+
6883
interface HubSpotSearchResult {
6984
id: string
7085
properties: Record<string, string | null>
@@ -227,7 +242,7 @@ async function pollSearchBased(
227242
? {
228243
propertySnapshot: {
229244
property: config.targetPropertyName?.trim() ?? '',
230-
values: {},
245+
entries: [],
231246
},
232247
}
233248
: {}),
@@ -248,13 +263,13 @@ async function pollSearchBased(
248263
)
249264
}
250265

251-
const properties = resolveRequestedProperties(config, objectType, filterProperty)
266+
const userFilters = buildUserFilters(config, logger, requestId)
267+
const properties = resolveRequestedProperties(config, objectType, filterProperty, userFilters)
252268
const maxRecords = Math.min(
253269
Math.max(config.maxRecordsPerPoll ?? DEFAULT_MAX_RECORDS, 1),
254270
MAX_MAX_RECORDS
255271
)
256272
const lastSeenObjectId = config.lastSeenObjectId
257-
const userFilters = buildUserFilters(config, logger, requestId)
258273

259274
const records = await fetchHubSpotChanges({
260275
accessToken,
@@ -313,7 +328,7 @@ async function pollSearchBased(
313328
lastCheckedTimestamp: new Date(nowMs).toISOString(),
314329
}
315330
if (snapshotForRun) {
316-
update.propertySnapshot = trimSnapshot(snapshotForRun)
331+
update.propertySnapshot = serializeSnapshot(snapshotForRun)
317332
}
318333
await updateWebhookProviderConfig(webhookId, update, logger)
319334

@@ -481,7 +496,8 @@ function resolveObjectType(config: HubSpotWebhookConfig): string {
481496
function resolveRequestedProperties(
482497
config: HubSpotWebhookConfig,
483498
objectType: string,
484-
filterProperty: string
499+
filterProperty: string,
500+
userFilters: FilterClause[]
485501
): string[] {
486502
const requested = new Set<string>()
487503

@@ -507,7 +523,7 @@ function resolveRequestedProperties(
507523
if (config.targetPropertyName?.trim()) {
508524
requested.add(config.targetPropertyName.trim())
509525
}
510-
for (const f of buildUserFilters(config)) {
526+
for (const f of userFilters) {
511527
if (f.propertyName) requested.add(f.propertyName)
512528
}
513529

@@ -567,26 +583,25 @@ function buildUserFilters(
567583
function resolvePropertySnapshot(
568584
config: HubSpotWebhookConfig,
569585
property: string
570-
): { property: string; values: Record<string, string | null> } {
586+
): PropertySnapshotState {
571587
const existing = config.propertySnapshot
572-
if (existing && existing.property === property) {
573-
return { property, values: { ...existing.values } }
588+
if (existing && existing.property === property && Array.isArray(existing.entries)) {
589+
return { property, values: new Map(existing.entries) }
574590
}
575591
// Property changed since last config — start fresh so we don't compare against stale values.
576-
return { property, values: {} }
592+
return { property, values: new Map() }
577593
}
578594

579-
function trimSnapshot(snapshot: { property: string; values: Record<string, string | null> }): {
595+
function serializeSnapshot(state: PropertySnapshotState): {
580596
property: string
581-
values: Record<string, string | null>
597+
entries: Array<[string, string | null]>
582598
} {
583-
const keys = Object.keys(snapshot.values)
584-
if (keys.length <= MAX_SNAPSHOT_SIZE) return snapshot
585-
// Drop oldest by insertion order (JS string-key iteration is insertion-ordered).
586-
const keep = keys.slice(keys.length - MAX_SNAPSHOT_SIZE)
587-
const trimmed: Record<string, string | null> = {}
588-
for (const k of keep) trimmed[k] = snapshot.values[k]
589-
return { property: snapshot.property, values: trimmed }
599+
let entries = [...state.values.entries()]
600+
// Drop oldest by insertion order; Map preserves it regardless of key type.
601+
if (entries.length > MAX_SNAPSHOT_SIZE) {
602+
entries = entries.slice(entries.length - MAX_SNAPSHOT_SIZE)
603+
}
604+
return { property: state.property, entries }
590605
}
591606

592607
interface FetchArgs {
@@ -721,7 +736,7 @@ async function processRecords(
721736
eventType: HubSpotEventType,
722737
filterProperty: string,
723738
targetProperty: string | undefined,
724-
snapshot: { property: string; values: Record<string, string | null> } | null,
739+
snapshot: PropertySnapshotState | null,
725740
requestId: string,
726741
logger: Logger
727742
): Promise<{
@@ -749,12 +764,13 @@ async function processRecords(
749764
let handledBySkip = false
750765
if (eventType === 'property_changed' && targetProperty && snapshot) {
751766
propertyValue = record.properties?.[targetProperty] ?? null
752-
const had = Object.hasOwn(snapshot.values, record.id)
753-
previousValue = had ? snapshot.values[record.id] : undefined
767+
const had = snapshot.values.has(record.id)
768+
previousValue = had ? snapshot.values.get(record.id) : undefined
754769
if (had && (previousValue ?? null) === (propertyValue ?? null)) {
755-
// Touch the snapshot to keep this record's entry from being trimmed before unchanged ones.
756-
delete snapshot.values[record.id]
757-
snapshot.values[record.id] = propertyValue ?? null
770+
// Touch the snapshot so this record's entry moves to the end of the LRU order.
771+
// Map.delete + Map.set re-inserts at the tail, regardless of key type.
772+
snapshot.values.delete(record.id)
773+
snapshot.values.set(record.id, propertyValue ?? null)
758774
skippedCount++
759775
handledBySkip = true
760776
}
@@ -807,7 +823,9 @@ async function processRecords(
807823
processedCount++
808824
handledSuccessfully = true
809825
if (eventType === 'property_changed' && targetProperty && snapshot) {
810-
snapshot.values[record.id] = propertyValue ?? null
826+
// Same delete+set dance so an updated value moves to the tail of the LRU order.
827+
snapshot.values.delete(record.id)
828+
snapshot.values.set(record.id, propertyValue ?? null)
811829
}
812830
} catch (error) {
813831
failedCount++

0 commit comments

Comments
 (0)