@@ -56,7 +56,7 @@ const MAX_DISPATCH_SUMMARY_GROUPS = 10
5656const MAX_DISPATCHED_EVENTS_PER_SECOND = 25
5757const PROJECT_AGENT_RECIPIENT_CACHE_TTL_MS = 2_000
5858const MAX_BROKER_SENDS_PER_SECOND = 25
59- const DELIVERY_INJECTED_CONFIRMATION_TIMEOUT_MS = 5_000
59+ const DEFAULT_DELIVERY_INJECTED_CONFIRMATION_TIMEOUT_MS = 5_000
6060const REMOTE_STREAM_ERROR_POLLING_FALLBACK_THRESHOLD = 5
6161const REMOTE_STREAM_POLL_INTERVAL_MS = 5_000
6262
@@ -130,6 +130,13 @@ type DeliveryDedupeClaim = {
130130 contentHash ?: string
131131}
132132
133+ type DeliveryDedupeClaimOutcome = 'committed' | 'released'
134+
135+ type InFlightDedupeClaim = {
136+ promise : Promise < DeliveryDedupeClaimOutcome >
137+ settle : ( outcome : DeliveryDedupeClaimOutcome ) => void
138+ }
139+
133140type DispatchItem = {
134141 event : ChangeEvent
135142 specs : SubscriptionSpec [ ]
@@ -375,6 +382,21 @@ function delay(ms: number): Promise<void> {
375382 return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) )
376383}
377384
385+ function deliveryInjectedConfirmationTimeoutMs ( ) : number {
386+ const value = Number . parseInt ( process . env . PEAR_INTEGRATION_EVENT_INJECTED_CONFIRMATION_TIMEOUT_MS || '' , 10 )
387+ return Number . isFinite ( value ) && value > 0 ? value : DEFAULT_DELIVERY_INJECTED_CONFIRMATION_TIMEOUT_MS
388+ }
389+
390+ function withTimeout < T > ( promise : Promise < T > , timeoutMs : number , message : string ) : Promise < T > {
391+ let timer : ReturnType < typeof setTimeout > | undefined
392+ const timeout = new Promise < never > ( ( _resolve , reject ) => {
393+ timer = setTimeout ( ( ) => reject ( new Error ( message ) ) , timeoutMs )
394+ } )
395+ return Promise . race ( [ promise , timeout ] ) . finally ( ( ) => {
396+ if ( timer ) clearTimeout ( timer )
397+ } )
398+ }
399+
378400function isRecord ( value : unknown ) : value is Record < string , unknown > {
379401 return ! ! value && typeof value === 'object' && ! Array . isArray ( value )
380402}
@@ -2215,6 +2237,7 @@ export class IntegrationEventBridge {
22152237 private dispatchers = new Map < string , ProjectEventDispatcher > ( )
22162238 private recentInjections = new Map < string , RecentInjectionState > ( )
22172239 private slackLogicalInjections = new Map < string , SlackLogicalInjectionState > ( )
2240+ private inFlightDedupeClaims = new Map < string , Set < Promise < DeliveryDedupeClaimOutcome > > > ( )
22182241 private projectAgentRecipientCache = new Map < string , ProjectAgentRecipientCacheEntry > ( )
22192242 private notificationTargetCache = new Map < string , NotificationTargetCacheEntry > ( )
22202243 private brokerSendPacers = new Map < string , ProjectBrokerSendPacer > ( )
@@ -2584,13 +2607,18 @@ export class IntegrationEventBridge {
25842607 dedupe = eventDedupeKeyWithFingerprint ( duplicateKey , fingerprint )
25852608 const slackClaim = this . claimSlackLogicalInjection ( dedupe . key , contextPreview , dedupe . ttlMs , shouldTrackDedupe )
25862609 if ( ! slackClaim . claimed ) {
2610+ const inFlightOutcome = await this . waitForInFlightDedupeClaims ( dedupe . key )
2611+ if ( inFlightOutcome === 'released' ) {
2612+ logIntegrationEvent ( 'retrying duplicate path after unconfirmed delivery' , {
2613+ projectId,
2614+ eventId : event . id ,
2615+ path : event . resource . path ,
2616+ duplicateKey : dedupe . key
2617+ } )
2618+ return this . injectEvent ( projectId , event , matchedSpecs )
2619+ }
25872620 incrementIntegrationEventCounter ( projectId , 'eventsDropped' )
2588- logIntegrationEvent ( 'skipped duplicate path' , {
2589- projectId,
2590- eventId : event . id ,
2591- path : event . resource . path ,
2592- duplicateKey : dedupe . key
2593- } )
2621+ this . reportSkippedDuplicatePath ( projectId , event , dedupe . key )
25942622 return
25952623 }
25962624 dedupeClaimed = shouldTrackDedupe
@@ -2604,13 +2632,18 @@ export class IntegrationEventBridge {
26042632 }
26052633 } else if ( shouldTrackDedupe ) {
26062634 if ( ! this . claimRecentInjection ( dedupe . key , dedupe . ttlMs , true ) ) {
2635+ const inFlightOutcome = await this . waitForInFlightDedupeClaims ( dedupe . key )
2636+ if ( inFlightOutcome === 'released' ) {
2637+ logIntegrationEvent ( 'retrying duplicate path after unconfirmed delivery' , {
2638+ projectId,
2639+ eventId : event . id ,
2640+ path : event . resource . path ,
2641+ duplicateKey : dedupe . key
2642+ } )
2643+ return this . injectEvent ( projectId , event , matchedSpecs )
2644+ }
26072645 incrementIntegrationEventCounter ( projectId , 'eventsDropped' )
2608- logIntegrationEvent ( 'skipped duplicate path' , {
2609- projectId,
2610- eventId : event . id ,
2611- path : event . resource . path ,
2612- duplicateKey : dedupe . key
2613- } )
2646+ this . reportSkippedDuplicatePath ( projectId , event , dedupe . key )
26142647 return
26152648 }
26162649 dedupeClaimed = true
@@ -2625,6 +2658,7 @@ export class IntegrationEventBridge {
26252658 // relay/replay identity rather than pinning a local claim.
26262659 dedupeClaimed = false
26272660 }
2661+ const inFlightClaim = deliveryClaim ? this . trackInFlightDedupeClaim ( deliveryClaim . key ) : undefined
26282662 const contextPreviewData = contextPreview ? eventContextPreviewMetadata ( contextPreview ) : undefined
26292663 const resolvedResource = isRecord ( event . resource )
26302664 ? { ...event . resource , path : resolvedPath }
@@ -2672,6 +2706,7 @@ export class IntegrationEventBridge {
26722706 // this event (remote copy of a local change, coalesced update) retries
26732707 // delivery instead of being dropped as a recent injection.
26742708 if ( dedupeClaimed ) this . releaseDedupeKey ( dedupe . key , needsSlackContentAwareDedupe )
2709+ inFlightClaim ?. settle ( 'released' )
26752710 throw sendErrors [ 0 ] . error
26762711 }
26772712 if ( sendErrors . length > 0 ) {
@@ -2688,9 +2723,11 @@ export class IntegrationEventBridge {
26882723 )
26892724 }
26902725 if ( deliveryClaim && injectedConfirmations . length > 0 ) {
2691- Promise . all ( injectedConfirmations )
2726+ void Promise . all ( injectedConfirmations )
26922727 . then ( ( ) => {
26932728 this . commitDedupeKey ( deliveryClaim )
2729+ incrementIntegrationEventCounter ( projectId , 'eventsInjected' )
2730+ inFlightClaim ?. settle ( 'committed' )
26942731 } )
26952732 . catch ( ( error ) => {
26962733 this . releaseDedupeKey ( deliveryClaim . key , deliveryClaim . isSlackLogicalKey , deliveryClaim . contentHash )
@@ -2705,11 +2742,27 @@ export class IntegrationEventBridge {
27052742 error : toErrorMessage ( error )
27062743 }
27072744 )
2745+ inFlightClaim ?. settle ( 'released' )
27082746 } )
27092747 } else if ( deliveryClaim ) {
27102748 this . releaseDedupeKey ( deliveryClaim . key , deliveryClaim . isSlackLogicalKey , deliveryClaim . contentHash )
2749+ inFlightClaim ?. settle ( 'released' )
2750+ } else {
2751+ incrementIntegrationEventCounter ( projectId , 'eventsInjected' )
27112752 }
2712- incrementIntegrationEventCounter ( projectId , 'eventsInjected' )
2753+ }
2754+
2755+ private reportSkippedDuplicatePath ( projectId : string , event : ChangeEvent , duplicateKey : string ) : void {
2756+ warnIntegrationEventAggregated (
2757+ `skipped duplicate path:${ projectId } ` ,
2758+ 'skipped duplicate path' ,
2759+ {
2760+ projectId,
2761+ eventId : event . id ,
2762+ path : event . resource . path ,
2763+ duplicateKey
2764+ }
2765+ )
27132766 }
27142767
27152768 private async recipientsForMatchedSpecs (
@@ -2817,11 +2870,16 @@ export class IntegrationEventBridge {
28172870 }
28182871
28192872 let injectedConfirmation : Promise < unknown > | undefined
2873+ const timeoutMs = deliveryInjectedConfirmationTimeoutMs ( )
28202874 await pacer . enqueue ( input , ( message ) => {
2821- injectedConfirmation = bridge . sendMessageAndWaitForInjected ! (
2822- projectId ,
2823- message ,
2824- { timeoutMs : DELIVERY_INJECTED_CONFIRMATION_TIMEOUT_MS }
2875+ injectedConfirmation = withTimeout (
2876+ bridge . sendMessageAndWaitForInjected ! (
2877+ projectId ,
2878+ message ,
2879+ { timeoutMs }
2880+ ) ,
2881+ timeoutMs + 250 ,
2882+ `Timed out waiting for broker delivery_injected confirmation for ${ message . to } `
28252883 )
28262884 return Promise . resolve ( )
28272885 } )
@@ -2937,6 +2995,32 @@ export class IntegrationEventBridge {
29372995 entry . expiresAt = now + claim . ttlMs
29382996 }
29392997
2998+ private trackInFlightDedupeClaim ( key : string ) : InFlightDedupeClaim {
2999+ let settle : ( outcome : DeliveryDedupeClaimOutcome ) => void = ( ) => undefined
3000+ const promise = new Promise < DeliveryDedupeClaimOutcome > ( ( resolve ) => {
3001+ settle = resolve
3002+ } )
3003+ let claims = this . inFlightDedupeClaims . get ( key )
3004+ if ( ! claims ) {
3005+ claims = new Set ( )
3006+ this . inFlightDedupeClaims . set ( key , claims )
3007+ }
3008+ claims . add ( promise )
3009+ void promise . finally ( ( ) => {
3010+ const current = this . inFlightDedupeClaims . get ( key )
3011+ current ?. delete ( promise )
3012+ if ( current ?. size === 0 ) this . inFlightDedupeClaims . delete ( key )
3013+ } )
3014+ return { promise, settle }
3015+ }
3016+
3017+ private async waitForInFlightDedupeClaims ( key : string ) : Promise < DeliveryDedupeClaimOutcome | null > {
3018+ const claims = Array . from ( this . inFlightDedupeClaims . get ( key ) ?? [ ] )
3019+ if ( claims . length === 0 ) return null
3020+ const outcomes = await Promise . all ( claims )
3021+ return outcomes . includes ( 'released' ) ? 'released' : 'committed'
3022+ }
3023+
29403024 private releaseDedupeKey ( key : string , isSlackLogicalKey : boolean , contentHash ?: string ) : void {
29413025 if ( isSlackLogicalKey ) {
29423026 const entry = this . slackLogicalInjections . get ( key )
0 commit comments