@@ -28,6 +28,7 @@ import { Client as TemporalClient } from '@crowd/temporal'
2828import {
2929 IMemberData ,
3030 IMemberIdentity ,
31+ IOrganization ,
3132 IOrganizationIdSource ,
3233 MemberAttributeName ,
3334 MemberBotDetection ,
@@ -43,6 +44,22 @@ import { IMemberCreateData, IMemberUpdateData } from './member.data'
4344import MemberAttributeService from './memberAttribute.service'
4445import { OrganizationService } from './organization.service'
4546
47+ /**
48+ * Returns a stable cache key for an org based on its verified identities, falling back to
49+ * displayName. Used by the org promise cache to deduplicate `findOrCreateOrganization` calls
50+ * across members in the same batch.
51+ */
52+ function orgCacheKey ( org : IOrganization ) : string | null {
53+ const verified = ( org . identities ?? [ ] )
54+ . filter ( ( i ) => i . verified )
55+ . map ( ( i ) => `${ i . platform } :${ i . type } :${ i . value . toLowerCase ( ) } ` )
56+ . sort ( )
57+ . join ( '|' )
58+ if ( verified ) return verified
59+ if ( org . displayName ) return `name:${ org . displayName . toLowerCase ( ) } `
60+ return null
61+ }
62+
4663export default class MemberService extends LoggerBase {
4764 private readonly memberRepo : MemberRepository
4865 private readonly pgQx : QueryExecutor
@@ -67,6 +84,7 @@ export default class MemberService extends LoggerBase {
6784 data : IMemberCreateData ,
6885 platform : PlatformType ,
6986 releaseMemberLock ?: ( ) => Promise < void > ,
87+ orgPromiseCache ?: Map < string , Promise < string | undefined > > ,
7088 ) : Promise < string > {
7189 return logExecutionTimeV2 (
7290 async ( ) => {
@@ -188,11 +206,31 @@ export default class MemberService extends LoggerBase {
188206 const orgService = new OrganizationService ( this . store , this . log )
189207 if ( data . organizations ) {
190208 for ( const org of data . organizations ) {
191- const id = await logExecutionTimeV2 (
192- ( ) => orgService . findOrCreate ( platform , integrationId , org ) ,
193- this . log ,
194- 'memberService -> create -> findOrCreateOrg' ,
195- )
209+ // Temp fix: skip the individual-noaccount.com placeholder org to avoid
210+ // hot-row contention on the organizations table. Permanent fix is in
211+ // tncTransformerBase.ts to stop emitting this org entirely.
212+ if (
213+ org . identities ?. some ( ( i ) => i . verified && i . value === 'individual-noaccount.com' )
214+ ) {
215+ continue
216+ }
217+
218+ const key = orgCacheKey ( org )
219+ let orgIdPromise : Promise < string | undefined >
220+ if ( key && orgPromiseCache ?. has ( key ) ) {
221+ orgIdPromise = orgPromiseCache . get ( key )
222+ } else {
223+ orgIdPromise = logExecutionTimeV2 (
224+ ( ) => orgService . findOrCreate ( platform , integrationId , org ) ,
225+ this . log ,
226+ 'memberService -> create -> findOrCreateOrg' ,
227+ )
228+ if ( key ) {
229+ orgPromiseCache ?. set ( key , orgIdPromise )
230+ orgIdPromise . catch ( ( ) => orgPromiseCache ?. delete ( key ) )
231+ }
232+ }
233+ const id = await orgIdPromise
196234 organizations . push ( {
197235 id,
198236 source : org . source ,
@@ -209,6 +247,7 @@ export default class MemberService extends LoggerBase {
209247 this . assignOrganizationByEmailDomain (
210248 integrationId ,
211249 emailIdentities . map ( ( i ) => i . value ) ,
250+ orgPromiseCache ,
212251 ) ,
213252 this . log ,
214253 'memberService -> create -> assignOrganizationByEmailDomain' ,
@@ -220,7 +259,6 @@ export default class MemberService extends LoggerBase {
220259
221260 if ( organizations . length > 0 ) {
222261 const uniqOrgs = uniqby ( organizations , 'id' )
223- const orgService = new OrganizationService ( this . store , this . log )
224262
225263 const orgsToAdd = (
226264 await Promise . all (
@@ -266,6 +304,7 @@ export default class MemberService extends LoggerBase {
266304 originalIdentities : IMemberIdentity [ ] ,
267305 platform : PlatformType ,
268306 releaseMemberLock ?: ( ) => Promise < void > ,
307+ orgPromiseCache ?: Map < string , Promise < string | undefined > > ,
269308 ) : Promise < void > {
270309 await logExecutionTimeV2 (
271310 async ( ) => {
@@ -398,13 +437,33 @@ export default class MemberService extends LoggerBase {
398437 const orgService = new OrganizationService ( this . store , this . log )
399438 if ( data . organizations ) {
400439 for ( const org of data . organizations ) {
440+ // Temp fix: skip the individual-noaccount.com placeholder org to avoid
441+ // hot-row contention on the organizations table. Permanent fix is in
442+ // tncTransformerBase.ts to stop emitting this org entirely.
443+ if (
444+ org . identities ?. some ( ( i ) => i . verified && i . value === 'individual-noaccount.com' )
445+ ) {
446+ continue
447+ }
448+
401449 this . log . trace ( { memberId : id } , 'Finding or creating organization!' )
402450
403- const orgId = await logExecutionTimeV2 (
404- ( ) => orgService . findOrCreate ( platform , integrationId , org ) ,
405- this . log ,
406- 'memberService -> update -> findOrCreateOrg' ,
407- )
451+ const key = orgCacheKey ( org )
452+ let orgIdPromise : Promise < string | undefined >
453+ if ( key && orgPromiseCache ?. has ( key ) ) {
454+ orgIdPromise = orgPromiseCache . get ( key )
455+ } else {
456+ orgIdPromise = logExecutionTimeV2 (
457+ ( ) => orgService . findOrCreate ( platform , integrationId , org ) ,
458+ this . log ,
459+ 'memberService -> update -> findOrCreateOrg' ,
460+ )
461+ if ( key ) {
462+ orgPromiseCache ?. set ( key , orgIdPromise )
463+ orgIdPromise . catch ( ( ) => orgPromiseCache ?. delete ( key ) )
464+ }
465+ }
466+ const orgId = await orgIdPromise
408467 organizations . push ( {
409468 id : orgId ,
410469 source : data . source ,
@@ -422,6 +481,7 @@ export default class MemberService extends LoggerBase {
422481 this . assignOrganizationByEmailDomain (
423482 integrationId ,
424483 emailIdentities . map ( ( i ) => i . value ) ,
484+ orgPromiseCache ,
425485 ) ,
426486 this . log ,
427487 'memberService -> update -> assignOrganizationByEmailDomain' ,
@@ -433,7 +493,6 @@ export default class MemberService extends LoggerBase {
433493
434494 if ( organizations . length > 0 ) {
435495 const uniqOrgs = uniqby ( organizations , 'id' )
436- const orgService = new OrganizationService ( this . store , this . log )
437496
438497 this . log . trace ( { memberId : id } , 'Finding member organizations!' )
439498 const orgsToAdd = (
@@ -473,6 +532,7 @@ export default class MemberService extends LoggerBase {
473532 public async assignOrganizationByEmailDomain (
474533 integrationId : string ,
475534 emails : string [ ] ,
535+ orgPromiseCache ?: Map < string , Promise < string | undefined > > ,
476536 ) : Promise < IOrganizationIdSource [ ] > {
477537 const orgService = new OrganizationService ( this . store , this . log )
478538 const organizations : IOrganizationIdSource [ ] = [ ]
@@ -494,26 +554,38 @@ export default class MemberService extends LoggerBase {
494554 // Assign member to organization based on email domain
495555 for ( const domain of emailDomains ) {
496556 const orgSource = OrganizationSource . EMAIL_DOMAIN
497- const orgId = await orgService . findOrCreate (
498- OrganizationAttributeSource . EMAIL ,
499- integrationId ,
500- {
501- attributes : {
502- name : {
503- integration : [ domain ] ,
504- } ,
557+ const org : IOrganization = {
558+ attributes : {
559+ name : {
560+ integration : [ domain ] ,
505561 } ,
506- identities : [
507- {
508- value : domain ,
509- type : OrganizationIdentityType . PRIMARY_DOMAIN ,
510- platform : 'email' ,
511- verified : true ,
512- source : orgSource ,
513- } ,
514- ] ,
515562 } ,
516- )
563+ identities : [
564+ {
565+ value : domain ,
566+ type : OrganizationIdentityType . PRIMARY_DOMAIN ,
567+ platform : 'email' ,
568+ verified : true ,
569+ source : orgSource ,
570+ } ,
571+ ] ,
572+ }
573+ const key = orgCacheKey ( org )
574+ let orgIdPromise : Promise < string | undefined >
575+ if ( key && orgPromiseCache ?. has ( key ) ) {
576+ orgIdPromise = orgPromiseCache . get ( key )
577+ } else {
578+ orgIdPromise = orgService . findOrCreate (
579+ OrganizationAttributeSource . EMAIL ,
580+ integrationId ,
581+ org ,
582+ )
583+ if ( key ) {
584+ orgPromiseCache ?. set ( key , orgIdPromise )
585+ orgIdPromise . catch ( ( ) => orgPromiseCache ?. delete ( key ) )
586+ }
587+ }
588+ const orgId = await orgIdPromise
517589 if ( orgId ) {
518590 organizations . push ( {
519591 id : orgId ,
0 commit comments