Skip to content

Commit 7b7e744

Browse files
committed
fix: resolve pr comments from ai bots
Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com>
1 parent 5c60ad6 commit 7b7e744

3 files changed

Lines changed: 101 additions & 112 deletions

File tree

Lines changed: 64 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import CronTime from 'cron-time-generator'
2-
import {
3-
inferMemberOrganizationStintChanges,
4-
MEMBER_ORG_STINT_CHANGES_DATES_PREFIX,
5-
MEMBER_ORG_STINT_CHANGES_QUEUE
6-
} from '@crowd/common_services'
2+
73
import {
8-
createMemberOrganization,
9-
fetchMemberOrganizationsBySource,
10-
updateMemberOrganization,
11-
} from '@crowd/data-access-layer'
4+
MEMBER_ORG_STINT_CHANGES_DATES_PREFIX,
5+
MEMBER_ORG_STINT_CHANGES_QUEUE,
6+
inferMemberOrganizationStintChanges,
7+
} from '@crowd/common_services'
8+
import { fetchMemberOrganizationsBySource } from '@crowd/data-access-layer'
129
import { WRITE_DB_CONFIG, getDbConnection } from '@crowd/data-access-layer/src/database'
13-
import { pgpQx, QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor'
14-
import { REDIS_CONFIG, RedisClient, getRedisClient } from '@crowd/redis'
15-
import { MemberOrgDate, MemberOrgStintChange, OrganizationSource } from '@crowd/types'
10+
import { pgpQx } from '@crowd/data-access-layer/src/queryExecutor'
11+
import { REDIS_CONFIG, getRedisClient } from '@crowd/redis'
12+
import { OrganizationSource } from '@crowd/types'
13+
1614
import { IJobDefinition } from '../types'
1715

1816
const job: IJobDefinition = {
@@ -24,106 +22,82 @@ const job: IJobDefinition = {
2422
const db = await getDbConnection(WRITE_DB_CONFIG(), 2, 0)
2523
const qx = pgpQx(db)
2624

27-
// 1. Fetch a batch of work triggers (Member IDs)
28-
const memberIds = await redis.sPop(MEMBER_ORG_STINT_CHANGES_QUEUE, 500)
25+
// 1. Get a batch of work
26+
const memberIds = await redis.sRandMember(MEMBER_ORG_STINT_CHANGES_QUEUE, 500)
2927
if (!memberIds?.length) return
3028

3129
ctx.log.info({ count: memberIds.length }, 'Processing pending members.')
3230
const stats = { processed: 0, inserts: 0, updates: 0 }
3331

3432
for (const memberId of memberIds) {
3533
try {
36-
// 2. Get the activity dates for this member
37-
const activityDates = await popMemberOrganizationActivityDates(redis, memberId)
38-
39-
// If the member has no activity dates, move to the next member
40-
if (activityDates.length === 0) continue
41-
42-
// 3. Sync with existing database state
43-
const existingOrgs = await fetchMemberOrganizationsBySource(
44-
qx,
45-
memberId,
46-
OrganizationSource.EMAIL_DOMAIN,
47-
)
48-
49-
// 4. Calculate required stint changes
50-
const stintChanges = inferMemberOrganizationStintChanges(
51-
memberId,
52-
existingOrgs,
53-
activityDates,
54-
)
55-
56-
if (stintChanges.length === 0) continue
57-
58-
const counts = {
59-
inserts: stintChanges.filter((c) => c.type === 'insert').length,
60-
updates: stintChanges.filter((c) => c.type === 'update').length,
34+
const datesKey = `${MEMBER_ORG_STINT_CHANGES_DATES_PREFIX}:${memberId}`
35+
const hash = await redis.hGetAll(datesKey)
36+
37+
// If no data, just remove from queue and move on
38+
if (!hash || Object.keys(hash).length === 0) {
39+
await redis.sRem(MEMBER_ORG_STINT_CHANGES_QUEUE, memberId)
40+
continue
6141
}
6242

63-
ctx.log.info({ memberId, ...counts }, 'Stint changes identified.')
43+
// 2. Parse Redis data into domain objects
44+
const { activityDates, orgIds } = parseMemberActivityHash(hash)
6445

65-
ctx.log.debug(
66-
{ memberId, activityDates, existingOrgs, stintChanges },
67-
'Stint inference trace.',
68-
)
46+
if (activityDates.length > 0) {
47+
// 3. Compare with DB and calculate delta
48+
const existingOrgs = await fetchMemberOrganizationsBySource(
49+
qx,
50+
memberId,
51+
OrganizationSource.EMAIL_DOMAIN,
52+
)
6953

70-
// @todo: Enable writes after dry-run validation
71-
// await applyStintChanges(qx, stintChanges)
54+
const changes = inferMemberOrganizationStintChanges(memberId, existingOrgs, activityDates)
55+
56+
if (changes.length > 0) {
57+
ctx.log.info({ memberId, count: changes.length }, 'Stint changes identified.')
58+
stats.inserts += changes.filter((c) => c.type === 'insert').length
59+
stats.updates += changes.filter((c) => c.type === 'update').length
60+
}
61+
}
62+
63+
// 4. Cleanup: Remove only the fields we actually read
64+
await redis
65+
.multi()
66+
.hDel(datesKey, ...orgIds)
67+
.sRem(MEMBER_ORG_STINT_CHANGES_QUEUE, memberId)
68+
.exec()
7269

7370
stats.processed++
74-
stats.inserts += counts.inserts
75-
stats.updates += counts.updates
7671
} catch (err) {
7772
ctx.log.error(err, { memberId }, 'Failed to process member stint inference.')
7873
}
7974
}
8075

81-
ctx.log.info(stats, 'Batch inference complete.')
76+
ctx.log.info(stats, 'Batch complete.')
8277
},
8378
}
8479

85-
async function popMemberOrganizationActivityDates(
86-
redis: RedisClient,
87-
memberId: string,
88-
): Promise<MemberOrgDate[]> {
89-
const key = `${MEMBER_ORG_STINT_CHANGES_DATES_PREFIX}:${memberId}`
90-
91-
// hGetAll + del in a multi block makes the "Pop" atomic for the entire Hash
92-
const [hash] = (await redis.multi().hGetAll(key).del(key).exec()) as [Record<string, string> | null, number]
93-
94-
if (!hash || Object.keys(hash).length === 0) return []
95-
96-
return Object.entries(hash)
97-
.flatMap(([organizationId, datesJson]) =>
98-
(JSON.parse(datesJson) as string[]).map((date) => ({ organizationId, date })),
99-
)
80+
/**
81+
* Parses the Redis hash into a clean, typed list of activity dates.
82+
*/
83+
function parseMemberActivityHash(hash: Record<string, string>) {
84+
const orgIds = Object.keys(hash)
85+
const activityDates = orgIds
86+
.flatMap((organizationId) => {
87+
try {
88+
const dates = JSON.parse(hash[organizationId])
89+
return Array.isArray(dates)
90+
? dates
91+
.filter((d): d is string => typeof d === 'string')
92+
.map((date) => ({ organizationId, date }))
93+
: []
94+
} catch {
95+
return []
96+
}
97+
})
10098
.sort((a, b) => a.date.localeCompare(b.date))
101-
}
102-
103-
async function applyStintChanges(
104-
qx: QueryExecutor,
105-
stintChanges: MemberOrgStintChange[],
106-
): Promise<void> {
107-
for (const change of stintChanges) {
108-
if (change.type === 'insert') {
109-
await createMemberOrganization(qx, change.memberId, {
110-
organizationId: change.organizationId,
111-
dateStart: change.dateStart,
112-
dateEnd: change.dateEnd,
113-
source: OrganizationSource.EMAIL_DOMAIN,
114-
})
115-
continue
116-
}
117-
118-
if (!change.id) {
119-
throw new Error('Missing id for update stint change.')
120-
}
12199

122-
await updateMemberOrganization(qx, change.memberId, change.id, {
123-
dateStart: change.dateStart,
124-
dateEnd: change.dateEnd,
125-
})
126-
}
100+
return { activityDates, orgIds }
127101
}
128102

129-
export default job
103+
export default job

services/apps/data_sink_worker/src/service/member.service.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,29 +1076,42 @@ export default class MemberService extends LoggerBase {
10761076
organizationId: string,
10771077
activityTimestamp: string,
10781078
): Promise<void> {
1079-
// 1. Normalize the timestamp to a simple YYYY-MM-DD string
1080-
const date = new Date(activityTimestamp).toISOString().slice(0, 10)
1079+
const date = new Date(activityTimestamp).toISOString().split('T')[0]
10811080
const key = `${MEMBER_ORG_STINT_CHANGES_DATES_PREFIX}:${memberId}`
10821081

1083-
// 2. Fetch and de-duplicate the date within the organization's buffer
1082+
// Safe parser for existing Redis strings.
1083+
// Returns a valid string array even if data is corrupt or missing.
1084+
const parseExistingDates = (value: string | null | undefined): string[] => {
1085+
if (!value) return []
1086+
try {
1087+
const parsed = JSON.parse(value)
1088+
return Array.isArray(parsed) ? parsed.filter((d): d is string => typeof d === 'string') : []
1089+
} catch {
1090+
this.log.warn('Corrupt dates buffer value detected during buffering.')
1091+
return []
1092+
}
1093+
}
1094+
1095+
// 1. Fetch current dates for this specific organization
10841096
const existing = await this.redisClient.hGet(key, organizationId)
1085-
const dates: string[] = existing ? JSON.parse(existing) : []
1097+
const dates: string[] = parseExistingDates(existing)
10861098

1087-
if (dates.includes(date)) {
1088-
this.log.trace({ memberId, organizationId, date }, 'Date already buffered, skipping.')
1089-
} else {
1099+
// 2. If the date is new, update the set and the queue
1100+
if (!dates.includes(date)) {
10901101
dates.push(date)
10911102
dates.sort()
10921103

1093-
// 3. Update the buffer with the new sorted date list
1094-
await this.redisClient.hSet(key, organizationId, JSON.stringify(dates))
1104+
await Promise.all([
1105+
// Update the specific org field in the hash
1106+
this.redisClient.hSet(key, organizationId, JSON.stringify(dates)),
1107+
// Ensure the member is in the processing queue
1108+
this.redisClient.sAdd(MEMBER_ORG_STINT_CHANGES_QUEUE, memberId),
1109+
])
1110+
10951111
this.log.debug(
1096-
{ memberId, organizationId, date, totalDates: dates.length },
1097-
'Buffered activity date for stint inference.',
1112+
{ memberId, organizationId, date, count: dates.length },
1113+
'Buffered activity date and queued member.',
10981114
)
10991115
}
1100-
1101-
// 4. add the member to the inference queue
1102-
await this.redisClient.sAdd(MEMBER_ORG_STINT_CHANGES_QUEUE, memberId)
11031116
}
11041117
}

services/libs/common_services/src/services/member-organization.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { RedisClient } from '@crowd/redis'
21
import {
32
IMemberOrganization,
43
MemberOrgDate,
@@ -130,6 +129,8 @@ interface Stint {
130129
isNew: boolean
131130
}
132131

132+
type DatedStint = Stint & { dateStart: string; dateEnd: string }
133+
133134
/**
134135
* Core logic to determine if activity dates should expand existing stints or create new ones
135136
*/
@@ -163,7 +164,9 @@ export function inferMemberOrganizationStintChanges(
163164
continue
164165

165166
// 3. Fill undated placeholder only when no dated stint exists yet (Rule 2)
166-
const dated = orgStints.filter((s) => s.dateStart && s.dateEnd)
167+
const dated = orgStints.filter(
168+
(s): s is DatedStint => s.dateStart !== null && s.dateEnd !== null,
169+
)
167170
const placeholder = orgStints.find((s) => !s.dateStart && !s.dateEnd)
168171
if (placeholder && dated.length === 0) {
169172
placeholder.dateStart = placeholder.dateEnd = targetDate
@@ -172,12 +175,12 @@ export function inferMemberOrganizationStintChanges(
172175
}
173176

174177
// 4. Find the closest neighbor stint to see if expansion is possible
175-
let neighbor: Stint | null = null
178+
let neighbor: DatedStint | null = null
176179
let minGap = Infinity
177180

178181
for (const s of dated) {
179182
const gap =
180-
targetDate > s.dateEnd! ? diff(s.dateEnd!, targetDate) : diff(targetDate, s.dateStart!)
183+
targetDate > s.dateEnd ? diff(s.dateEnd, targetDate) : diff(targetDate, s.dateStart)
181184
if (gap < minGap) {
182185
minGap = gap
183186
neighbor = s
@@ -198,8 +201,9 @@ export function inferMemberOrganizationStintChanges(
198201

199202
// 5. Determine the gap window between neighbor and targetDate, then check if another org
200203
// holds a significant (>= 30 day) dated stint that overlaps that window (multi-stint guard)
201-
const gapStart = targetDate > neighbor.dateEnd! ? neighbor.dateEnd! : targetDate
202-
const gapEnd = targetDate > neighbor.dateEnd! ? targetDate : neighbor.dateStart!
204+
const isForward = targetDate > neighbor.dateEnd
205+
const gapStart = isForward ? neighbor.dateEnd : targetDate
206+
const gapEnd = isForward ? targetDate : neighbor.dateStart
203207
const hasConflict = stints.some(
204208
(s) =>
205209
s.organizationId !== organizationId &&
@@ -210,8 +214,6 @@ export function inferMemberOrganizationStintChanges(
210214
diff(s.dateStart, s.dateEnd) >= 30,
211215
)
212216

213-
const isForward = targetDate > neighbor.dateEnd!
214-
215217
if (hasConflict) {
216218
// 6a. Another org owns the gap — start a fresh stint rather than bridging
217219
stints.push({

0 commit comments

Comments
 (0)