@@ -15,8 +15,8 @@ import {
1515} from '@crowd/data-access-layer'
1616import { WRITE_DB_CONFIG , getDbConnection } from '@crowd/data-access-layer/src/database'
1717import { pgpQx } from '@crowd/data-access-layer/src/queryExecutor'
18- import { REDIS_CONFIG , getRedisClient } from '@crowd/redis'
19- import { MemberOrgStintChange , OrganizationSource } from '@crowd/types'
18+ import { REDIS_CONFIG , RedisCache , getRedisClient } from '@crowd/redis'
19+ import { MemberOrgDate , MemberOrgStintChange , OrganizationSource } from '@crowd/types'
2020
2121import { IJobDefinition } from '../types'
2222
@@ -41,40 +41,44 @@ const job: IJobDefinition = {
4141 for ( const memberId of memberIds ) {
4242 try {
4343 const datesKey = `${ MEMBER_ORG_STINT_CHANGES_DATES_PREFIX } :${ memberId } `
44- const hash = await redis . hGetAll ( datesKey )
44+ const rawMembers = await redis . sMembers ( datesKey )
4545
46- if ( ! hash || Object . keys ( hash ) . length === 0 ) {
46+ if ( ! rawMembers ?. length ) {
4747 await redis . sRem ( MEMBER_ORG_STINT_CHANGES_QUEUE , memberId )
4848 continue
4949 }
5050
51- const { activityDates , orgIds } = parseMemberActivityHash ( hash )
51+ const orgDates = parseSetMembers ( rawMembers )
5252
53- if ( activityDates . length > 0 ) {
53+ if ( orgDates . length > 0 ) {
5454 const existingOrgs = await fetchMemberOrganizationsBySource (
5555 qx ,
5656 memberId ,
5757 OrganizationSource . EMAIL_DOMAIN ,
5858 )
5959
60- const changes = inferMemberOrganizationStintChanges ( memberId , existingOrgs , activityDates )
60+ const changes = inferMemberOrganizationStintChanges ( memberId , existingOrgs , orgDates )
6161
6262 if ( changes . length > 0 ) {
6363 ctx . log . debug ( { memberId, changes } , 'Stint changes identified.' )
64- await applyStintChanges ( qx , changes )
64+ await qx . tx ( ( tx ) => applyStintChanges ( tx , changes ) )
6565 }
6666 }
6767
68- // Remove only the fields we actually read
69- await redis
70- . multi ( )
71- . hDel ( datesKey , orgIds )
72- . sRem ( MEMBER_ORG_STINT_CHANGES_QUEUE , memberId )
73- . exec ( )
68+ // Atomically remove only the values we read.
69+ // If no new values were added, remove the member from the queue.
70+ await RedisCache . ackSetMembers (
71+ redis ,
72+ datesKey ,
73+ MEMBER_ORG_STINT_CHANGES_QUEUE ,
74+ memberId ,
75+ rawMembers ,
76+ )
7477
7578 processed ++
7679 } catch ( err ) {
7780 ctx . log . error ( err , { memberId } , 'Failed to process member stint inference.' )
81+ throw err
7882 }
7983 }
8084
@@ -83,23 +87,19 @@ const job: IJobDefinition = {
8387}
8488
8589/**
86- * Parses the Redis hash into a clean, typed list of activity dates.
90+ * Parses set members of the form "orgId|date" into typed activity dates.
8791 */
88- function parseMemberActivityHash ( hash : Record < string , string > ) {
89- const orgIds = Object . keys ( hash )
90- const activityDates = orgIds . flatMap ( ( organizationId ) => {
91- try {
92- const dates = JSON . parse ( hash [ organizationId ] )
93- return Array . isArray ( dates )
94- ? dates
95- . filter ( ( d ) : d is string => typeof d === 'string' )
96- . map ( ( date ) => ( { organizationId, date } ) )
97- : [ ]
98- } catch {
99- return [ ]
92+ function parseSetMembers ( members : string [ ] ) : MemberOrgDate [ ] {
93+ const results : MemberOrgDate [ ] = [ ]
94+
95+ for ( const m of members ) {
96+ const idx = m . indexOf ( '|' )
97+ if ( idx > 0 ) {
98+ results . push ( { organizationId : m . slice ( 0 , idx ) , date : m . slice ( idx + 1 ) } )
10099 }
101- } )
102- return { activityDates, orgIds }
100+ }
101+
102+ return results
103103}
104104
105105/**
0 commit comments